fix: bound and gate the crop evidence lane
The crop lane asked the phone for a full-resolution crop of a facing the frame could not decide. Four defects made that unsafe or useless: * A capability was half-applied. `crop: true` without a usable `max_crop_bytes` enabled the lane on the client's say-so, leaving the two sides believing different limits. A capability is now believed whole or refused with BAD_MESSAGE, and the limit must be an integer in 1..1048576 (bool is an int in Python and is not a limit). * A small JPEG could claim enormous dimensions. The header is now read without decoding and anything over 4 megapixels is refused before the decode allocates it. * A request was single-use, so an expired one stranded a facing only the crop lane could decide. The lane now spends the same max_attempts budget the frame path spends, refilled on method change and on settled-label drift, and a phone that never answers still cannot loop. * A candidate could be asked about from a box the frame path would refuse: clipped by the frame edge, carried over from an earlier frame, or measured at a score the tracker does not trust. Both lanes now share one edge gate, and only a track whose box this frame actually measured is asked about. Drift and method change also cleared `identity` but left `confirmed = True`, which permanently blocked both the crop lane and label inheritance for that track; both now clear it, without which the budget reset was dead code. Blur and glare are refused as whole-capture faults, while `too_far` is exempt on purpose: a distant shelf is the one frame a native-resolution crop exists to decide.
This commit is contained in:
parent
02e69acac7
commit
aa25f99699
5 changed files with 504 additions and 46 deletions
|
|
@ -366,8 +366,35 @@ class WebSocketProtocolTests(unittest.TestCase):
|
|||
self.assertEqual(ack["type"], "capabilities_ack")
|
||||
self.assertEqual(ack["capabilities"], {"crop": True, "max_crop_bytes": 65536})
|
||||
|
||||
def test_a_capability_the_server_cannot_honour_is_refused(self):
|
||||
"""A half-stated or malformed capability turns nothing on."""
|
||||
for message in (
|
||||
{"type": "capabilities", "max_crop_bytes": 65536},
|
||||
{"type": "capabilities", "crop": False, "max_crop_bytes": 65536},
|
||||
{"type": "capabilities", "crop": "true", "max_crop_bytes": 65536},
|
||||
{"type": "capabilities", "crop": True},
|
||||
{"type": "capabilities", "crop": True, "max_crop_bytes": 0},
|
||||
{"type": "capabilities", "crop": True, "max_crop_bytes": -4096},
|
||||
{"type": "capabilities", "crop": True, "max_crop_bytes": True},
|
||||
{"type": "capabilities", "crop": True, "max_crop_bytes": "65536"},
|
||||
{"type": "capabilities", "crop": True, "max_crop_bytes": 64 * 1024 * 1024},
|
||||
):
|
||||
with self.subTest(capability=message):
|
||||
with self.crop_app().websocket_connect("/v1/stream") as socket:
|
||||
socket.receive_json() # hello
|
||||
socket.send_json(message)
|
||||
self.assertEqual(socket.receive_json()["code"], "BAD_MESSAGE")
|
||||
# A refused capability leaves this session on protocol v1: the
|
||||
# facing it cannot report is never asked about.
|
||||
for frame_id in (1, 2, 3):
|
||||
socket.send_bytes(frame_payload(frame_id))
|
||||
result = socket.receive_json()
|
||||
self.assertEqual(result["stats"]["crops"]["requested"], 0)
|
||||
for track in result["tracks"]:
|
||||
self.assertNotIn("crop_request", track)
|
||||
|
||||
def test_a_v1_client_is_never_asked_for_a_crop(self):
|
||||
with self.client.websocket_connect("/v1/stream") as socket:
|
||||
with self.crop_app().websocket_connect("/v1/stream") as socket:
|
||||
socket.receive_json() # hello
|
||||
for frame_id in (1, 2, 3):
|
||||
socket.send_bytes(frame_payload(frame_id))
|
||||
|
|
@ -379,7 +406,7 @@ class WebSocketProtocolTests(unittest.TestCase):
|
|||
def test_a_crop_round_trip_labels_the_track(self):
|
||||
with self.crop_app().websocket_connect("/v1/stream") as socket:
|
||||
socket.receive_json() # hello
|
||||
socket.send_json({"type": "capabilities", "crop": True})
|
||||
socket.send_json({"type": "capabilities", "crop": True, "max_crop_bytes": 131072})
|
||||
self.assertEqual(socket.receive_json()["type"], "capabilities_ack")
|
||||
socket.send_bytes(frame_payload(1))
|
||||
socket.receive_json()
|
||||
|
|
@ -406,7 +433,7 @@ class WebSocketProtocolTests(unittest.TestCase):
|
|||
def test_a_crop_that_does_not_match_its_request_is_refused(self):
|
||||
with self.crop_app().websocket_connect("/v1/stream") as socket:
|
||||
socket.receive_json() # hello
|
||||
socket.send_json({"type": "capabilities", "crop": True})
|
||||
socket.send_json({"type": "capabilities", "crop": True, "max_crop_bytes": 131072})
|
||||
socket.receive_json()
|
||||
socket.send_bytes(frame_payload(1))
|
||||
socket.receive_json()
|
||||
|
|
@ -429,7 +456,7 @@ class WebSocketProtocolTests(unittest.TestCase):
|
|||
def test_a_crop_that_is_not_an_image_is_refused(self):
|
||||
with self.crop_app().websocket_connect("/v1/stream") as socket:
|
||||
socket.receive_json() # hello
|
||||
socket.send_json({"type": "capabilities", "crop": True})
|
||||
socket.send_json({"type": "capabilities", "crop": True, "max_crop_bytes": 131072})
|
||||
socket.receive_json()
|
||||
socket.send_bytes(frame_payload(1))
|
||||
socket.receive_json()
|
||||
|
|
@ -445,6 +472,28 @@ class WebSocketProtocolTests(unittest.TestCase):
|
|||
})
|
||||
self.assertEqual(socket.receive_json()["code"], "CROP_UNDECODABLE")
|
||||
|
||||
def test_a_small_jpeg_claiming_huge_pixels_is_refused_before_decode(self):
|
||||
with self.crop_app().websocket_connect("/v1/stream") as socket:
|
||||
socket.receive_json()
|
||||
socket.send_json({"type": "capabilities", "crop": True, "max_crop_bytes": 131072})
|
||||
socket.receive_json()
|
||||
socket.send_bytes(frame_payload(1))
|
||||
socket.receive_json()
|
||||
socket.send_bytes(frame_payload(2))
|
||||
request = socket.receive_json()["tracks"][0]["crop_request"]
|
||||
jpeg = bytearray(jpeg_bytes(80, 120))
|
||||
sof = jpeg.index(b"\xff\xc0")
|
||||
jpeg[sof + 5:sof + 9] = (16384).to_bytes(2, "big") * 2
|
||||
socket.send_json({
|
||||
"type": "crop", "request_id": request["request_id"],
|
||||
"track_id": request["track_id"], "frame_id": request["frame_id"],
|
||||
"jpeg": base64.b64encode(jpeg).decode("ascii"),
|
||||
})
|
||||
refused = socket.receive_json()
|
||||
self.assertEqual(refused["code"], "CROP_TOO_LARGE")
|
||||
socket.send_json({"type": "ping", "t": 1})
|
||||
self.assertEqual(socket.receive_json(), {"type": "pong", "t": 1})
|
||||
|
||||
def test_a_crop_without_an_image_is_refused(self):
|
||||
with self.client.websocket_connect("/v1/stream") as socket:
|
||||
socket.receive_json() # hello
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue