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
|
|
@ -94,6 +94,37 @@ MAX_FRAME_BYTES = 4 * 1024 * 1024
|
|||
DEFAULT_MAX_SESSIONS = 4
|
||||
WS_IDLE_TIMEOUT_S = 120.0
|
||||
CATALOG_VERSION_FALLBACK = "unknown"
|
||||
MAX_CROP_PIXELS = 4_000_000
|
||||
|
||||
|
||||
def _jpeg_dimensions(data: bytes) -> Optional[Tuple[int, int]]:
|
||||
"""Read JPEG SOF dimensions without allocating a decoded image."""
|
||||
if not data.startswith(b"\xff\xd8"):
|
||||
return None
|
||||
offset = 2
|
||||
while offset + 4 <= len(data):
|
||||
if data[offset] != 0xff:
|
||||
return None
|
||||
while offset < len(data) and data[offset] == 0xff:
|
||||
offset += 1
|
||||
if offset >= len(data):
|
||||
return None
|
||||
marker = data[offset]
|
||||
offset += 1
|
||||
if marker in (0xd8, 0x01) or 0xd0 <= marker <= 0xd7:
|
||||
continue
|
||||
if marker in (0xd9, 0xda) or offset + 2 > len(data):
|
||||
return None
|
||||
length = int.from_bytes(data[offset:offset + 2], "big")
|
||||
if length < 2 or offset + length > len(data):
|
||||
return None
|
||||
if marker in (0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf):
|
||||
if length < 7:
|
||||
return None
|
||||
return (int.from_bytes(data[offset + 5:offset + 7], "big"),
|
||||
int.from_bytes(data[offset + 3:offset + 5], "big"))
|
||||
offset += length
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -821,13 +852,20 @@ def create_app(
|
|||
"""A client that can answer with a full-resolution crop says so here.
|
||||
|
||||
Until this arrives the session keeps the protocol-v1 behaviour: no
|
||||
result carries a crop request, so a v1 client is never asked.
|
||||
result carries a crop request, so a v1 client is never asked. A
|
||||
capability is believed whole or not at all: a message that does not
|
||||
name the lane with a limit the server can honour is refused and the
|
||||
lane stays off, rather than half-applied on the client's say-so.
|
||||
"""
|
||||
try:
|
||||
capabilities = session.enable_crops(message.get("max_crop_bytes"))
|
||||
except (TypeError, ValueError):
|
||||
if message.get("crop") is not True or "max_crop_bytes" not in message:
|
||||
await send({"type": "error", "code": "BAD_MESSAGE",
|
||||
"message": "bad max_crop_bytes"})
|
||||
"message": "crop must be true and state max_crop_bytes"})
|
||||
return
|
||||
try:
|
||||
capabilities = session.enable_crops(message["max_crop_bytes"])
|
||||
except ValueError:
|
||||
await send({"type": "error", "code": "BAD_MESSAGE",
|
||||
"message": "max_crop_bytes must be 1..%d" % MAX_CROP_BYTES})
|
||||
return
|
||||
await send({"type": "capabilities_ack", "capabilities": capabilities})
|
||||
|
||||
|
|
@ -855,6 +893,11 @@ def create_app(
|
|||
await send({"type": "crop_error", "request_id": request_id,
|
||||
"code": "CROP_BAD_MESSAGE", "message": "jpeg is not valid base64"})
|
||||
return
|
||||
dimensions = _jpeg_dimensions(crop_bytes)
|
||||
if dimensions is not None and dimensions[0] * dimensions[1] > MAX_CROP_PIXELS:
|
||||
await send({"type": "crop_error", "request_id": request_id,
|
||||
"code": "CROP_TOO_LARGE", "message": "crop dimensions exceed the limit"})
|
||||
return
|
||||
reply = await loop.run_in_executor(
|
||||
None, session.submit_crop, request_id, message.get("track_id"),
|
||||
message.get("frame_id"), crop_bytes,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue