From baceed45c4c6e9153bdc52a32f52e5ad575456cb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 06:56:40 +0000 Subject: [PATCH] refactor(n2): dedupe data-URL header validation in n2_payload.py _decode_data_url and _data_url_media_type each hand-rolled the identical "data:...,...;base64..." validation and the same header[5:].split(";", 1)[0] media-type extraction -- the second copy was added right alongside the first in #374 (prune-to-budget) without noticing the first already existed a few lines up. _decode_data_url now calls _data_url_media_type for validation and media-type extraction, then does only its own base64 decode, so there is one definition of what a valid n2 screenshot data URL looks like. No public behavior change: both functions are module-private (not exported from yutori.navigator), same exceptions raised under the same conditions, same return values. Full non-slow suite (914 passed, 3 skipped) and the n2 payload/harness/compaction suites pass unmodified; ruff check/format clean. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01DvN8Ceba13sWCcsPbFmBUP --- yutori/navigator/n2_payload.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/yutori/navigator/n2_payload.py b/yutori/navigator/n2_payload.py index 467fada..e671aec 100644 --- a/yutori/navigator/n2_payload.py +++ b/yutori/navigator/n2_payload.py @@ -43,13 +43,23 @@ DEFAULT_MAX_MESSAGES_BYTES = MAX_REQUEST_BODY_BYTES - REQUEST_ENVELOPE_ALLOWANCE_BYTES -def _decode_data_url(url: str) -> "tuple[bytes, str]": +def _data_url_media_type(url: str) -> str: + """The media type of a base64 data URL, without decoding its payload.""" if not isinstance(url, str) or not url.startswith("data:") or "," not in url: raise ValueError("n2 screenshots must be base64 data URLs") - header, encoded = url.split(",", 1) + header = url.split(",", 1)[0] if ";base64" not in header: raise ValueError("n2 screenshots must use base64 data URLs") - return base64.b64decode(encoded), header[5:].split(";", 1)[0] + return header[5:].split(";", 1)[0] + + +def _decode_data_url(url: str) -> "tuple[bytes, str]": + # Validation and header parsing live in _data_url_media_type; this only adds + # the base64 decode, so the two share one definition of what a valid n2 + # screenshot data URL looks like. + media_type = _data_url_media_type(url) + _, encoded = url.split(",", 1) + return base64.b64decode(encoded), media_type def image_dimensions(url: str) -> "tuple[int, int]": @@ -59,16 +69,6 @@ def image_dimensions(url: str) -> "tuple[int, int]": return image.size -def _data_url_media_type(url: str) -> str: - """The media type of a base64 data URL, without decoding its payload.""" - if not isinstance(url, str) or not url.startswith("data:") or "," not in url: - raise ValueError("n2 screenshots must be base64 data URLs") - header = url.split(",", 1)[0] - if ";base64" not in header: - raise ValueError("n2 screenshots must use base64 data URLs") - return header[5:].split(";", 1)[0] - - def prepare_n2_image_data_url(url: str, image_format: str = DEFAULT_IMAGE_FORMAT) -> str: """Re-encode an image data URL to ``image_format``; returned unchanged when it already is.