diff --git a/docs/adr/0019-cloudflare-pingora-edge-standard.md b/docs/adr/0019-cloudflare-pingora-edge-standard.md index 805e538b86..9f92f0f046 100644 --- a/docs/adr/0019-cloudflare-pingora-edge-standard.md +++ b/docs/adr/0019-cloudflare-pingora-edge-standard.md @@ -33,6 +33,12 @@ so a governed shared implementation is required. 6. Initial migration does not use Pingora's experimental cache integration. 7. PHP workloads move to an HTTP application server or reviewed FastCGI adapter behind Pingora before the public listener changes. +8. Documentation PNG screenshots and PDF papers without a text diff are verified + from bounded format evidence (a complete CRC-valid PNG chunk stream with + conforming chunk names, palette bounds, and palette indices whose bounded null- or + Adam7-interlaced decompressed scanlines match IHDR, or a PDF signature) and excluded + from runtime-content scanning; + runtime paths and malformed or unsupported binary evidence still fail closed. ## Consequences diff --git a/docs/doctoring/pingora-documentation-image-evidence.md b/docs/doctoring/pingora-documentation-image-evidence.md new file mode 100644 index 0000000000..af10942cd8 --- /dev/null +++ b/docs/doctoring/pingora-documentation-image-evidence.md @@ -0,0 +1,17 @@ +# Pingora documentation image evidence + +The required Pingora gate previously sent a changed PNG screenshot through its +UTF-8 runtime-content decoder because GitHub omits text patches for binary files. +That rejected UI evidence before the policy could determine whether it described +an active edge runtime. + +ADR-0019 now admits documentation PNG screenshots only when the bounded final +file is a complete CRC-valid PNG chunk stream ending at IEND with no trailing +payload, conforming chunk names, palette bounds and indices, and bounded null- or +Adam7-interlaced decompressed scanlines that match IHDR. A signature or +CRC-valid arbitrary IDAT is insufficient. Files in a runtime path, malformed signatures, +unsupported binary formats, and unavailable evidence continue to fail closed. +The gate establishes bounded binary evidence rather than general image-rendering +fidelity; optional ancillary-chunk semantics are outside this policy boundary. +`tests/test_pingora_edge_policy.py` covers the accepted PNG and the existing fake +PDF/runtime cases; targeted branch coverage remains 100%. diff --git a/docs/policies/PINGORA_EDGE_POLICY.md b/docs/policies/PINGORA_EDGE_POLICY.md index 4d4c0752e1..619374a13d 100644 --- a/docs/policies/PINGORA_EDGE_POLICY.md +++ b/docs/policies/PINGORA_EDGE_POLICY.md @@ -53,8 +53,15 @@ The organization-required `required-workflow-bootstrap` job runs trusted base-branch scanner code at the immutable required-workflow SHA. It reads bounded changed-file metadata and final UTF-8 content through GitHub's REST API. It does not check out or execute pull-request content and receives only read permissions. -Malformed, truncated, binary, symlink, oversized, or unavailable evidence fails -closed. +Malformed, truncated, symlinked, oversized, or unavailable runtime evidence fails +closed. Documentation PNG screenshots and PDF papers without a text diff are +excluded only after bounded format verification; PNG evidence must be a complete +CRC-valid chunk stream ending at IEND with conforming chunk names, palette +bounds, and palette indices whose bounded null- or Adam7-interlaced decompressed +scanlines match IHDR. +This is a bounded binary-evidence classifier, not a general image renderer; +visual fidelity and optional ancillary-chunk semantics are outside this gate. +Other binary files remain unavailable evidence and fail closed. ## Exception process diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 41d95b6f57..2a8f4c7b54 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -93,6 +93,7 @@ flowchart LR | G-13 | hourly scheduler는 존재하지만 no-op/credential unavailable/queued Checks의 customer next action을 모든 caller가 동일한 receipt로 내는지 미확인이다 | 자동화가 실패해도 운영자가 무엇을 고쳐야 하는지 알 수 없다 | `skipped_credential_unavailable` receipt와 다음 행동 문구를 exact-head Checks로 검증하고, bounded receipt schema, retry floor, single-flight, no secret fallback을 모든 caller contract test로 고정한다 | | G-14 | release/changelog/version 증거가 각 PR에 분산되고 현재 central repo 보호 main의 release candidate가 명확하지 않다 | 운영자는 어떤 기능이 supportable release인지 확인할 수 없다 | merge 후 release readiness ledger, CHANGELOG, semantic version/tag, rollback/operability evidence를 함께 갱신한다 | | G-15 | 첨부파일 처리 경계가 제품별로 다르고, 1MB 상한은 업무 데이터와 맞지 않으며 미지원 MIME/컨테이너가 parser registry에서 명시적으로 pending/quarantine 되는지 확인되지 않았다. 현재 20MB 초과 파일 가능성과 PDF/HWP/HWPX·이미지·압축파일의 parse/sidecar 흐름을 하나의 exact contract로 묶지 못했다 | 큰 업무 첨부를 거부하거나 파싱 실패를 조용히 잃으면 고객의 메일·문서 업무가 중단된다 | naruon/newsdom-api 소유 PR에서 streaming upload, configurable bounded limit above 20MB, MIME sniffing, parser capability registry, quarantine/retry, source-position provenance, and ADR를 추가하고 size/unsupported-type/zip-bomb tests를 required evidence로 만든다 | +| G-16 | Required Pingora policy treated a changed documentation PNG screenshot as UTF-8 runtime evidence | Valid UI evidence blocked otherwise valid product PRs before policy evaluation | This branch verifies bounded PNG magic before exemption while runtime paths and malformed assets continue to fail closed; protected-main delivery remains the release gate | ## 4. 열린 PR live inventory diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index 823e17fbe5..33e58ed876 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -15,6 +15,7 @@ import os import re import sys +import zlib from dataclasses import dataclass from pathlib import PurePosixPath from typing import Callable, Mapping, Sequence @@ -38,7 +39,11 @@ # 1 MiB base64 ceiling -- rejecting a legitimate research-paper citation # (this org's own "attach the relevant paper PDF" convention) for a reason # that has nothing to do with the Nginx runtime policy this module enforces. -BINARY_DOCUMENT_SUFFIXES = frozenset({".pdf"}) +BINARY_DOCUMENT_MAGIC = { + ".pdf": (b"%PDF-",), + ".png": (b"\x89PNG\r\n\x1a\n",), +} +PNG_SIGNATURE = BINARY_DOCUMENT_MAGIC[".png"][0] SOURCE_TEST_SUFFIXES = frozenset({".py", ".pyi", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".rs"}) LICENSE_NAMES = frozenset({"license", "license.md", "copying", "copyrights", "notice"}) DOCUMENTATION_DIRECTORIES = frozenset({"doc", "docs", "documentation"}) @@ -171,7 +176,7 @@ def _is_documentation_or_source_fixture(path: str) -> bool: """Return whether *path* is prose, license text, or scanner source fixture. Textual suffixes only: a ``.pdf`` is handled separately by - ``_is_binary_documentation_pdf`` and gated on GitHub reporting no diff + ``_is_binary_documentation_asset`` and gated on GitHub reporting no diff ``patch`` for it, so a textual file merely named with a ``.pdf`` suffix (one GitHub *can* diff, meaning it could carry inspectable content) is never exempted here. @@ -206,15 +211,15 @@ def _is_documentation_or_source_fixture(path: str) -> bool: return False -def _is_binary_documentation_pdf(changed: ChangedFile) -> bool: - """Return whether *changed* is a plausibly binary documentation PDF. +def _is_binary_documentation_asset(changed: ChangedFile) -> bool: + """Return whether *changed* is a plausibly binary documentation asset. This is only the cheap, patch-presence pre-filter: GitHub's changed-files API never returns a diff ``patch`` for a true binary file, so a missing ``patch`` is *necessary* but not *sufficient* evidence -- GitHub also omits one for a textual diff that merely exceeds its own rendering limit. A caller with network access (``evaluate_pull_request``) must - still confirm this with ``_pdf_evidence_confirms_binary`` before + still confirm this with ``_binary_documentation_evidence_confirms`` before trusting it; a caller without one (this module's own unit tests calling this function directly) is only checking the necessary condition. """ @@ -223,8 +228,9 @@ def _is_binary_documentation_pdf(changed: ChangedFile) -> bool: return False pure = PurePosixPath(changed.path) return ( - pure.suffix.lower() in BINARY_DOCUMENT_SUFFIXES + pure.suffix.lower() in BINARY_DOCUMENT_MAGIC and _is_known_documentation_path(pure) + and _runtime_path_rule(changed.path) is None ) @@ -414,10 +420,7 @@ def _load_file_content(api_url: str, repository: str, path: str, head_sha: str, raise PolicyError(f"Runtime policy candidate {path} is not valid UTF-8") from exc -_PDF_MAGIC_PREFIX = b"%PDF-" - - -def _pdf_evidence_confirms_binary( +def _binary_documentation_evidence_confirms( changed: ChangedFile, *, api_url: str, @@ -426,17 +429,18 @@ def _pdf_evidence_confirms_binary( token: str, opener: OpenJson, ) -> bool: - """Return whether a claimed binary documentation PDF is genuinely binary. + """Return whether a claimed binary documentation asset is genuine. A missing diff ``patch`` alone is not proof of binary content: GitHub also omits a patch for a textual diff that exceeds its own rendering limit, well under this module's ``MAX_FILE_BYTES`` content-fetch ceiling. Whenever the file's raw bytes can be fetched at all, this - verifies the real ``%PDF-`` magic prefix instead of trusting + verifies the declared format's magic prefix instead of trusting patch-presence alone. Only a file whose content evidently exceeds the - Contents API's size ceiling -- the exact case ``_is_binary_documentation_pdf`` + Contents API's size ceiling -- the exact case ``_is_binary_documentation_asset`` exists for, a cited, large research paper -- falls back to trusting the - path+suffix convention; every other content-evidence failure (a + path+suffix convention for oversized PDFs only; every other + content-evidence failure (a malformed API response, corrupt base64, a declared size that does not match the decoded bytes) propagates and fails the whole check closed, same as for any other file that needs scanning. @@ -445,22 +449,169 @@ def _pdf_evidence_confirms_binary( try: raw = _load_raw_file_bytes(api_url, repository, changed.path, head_sha, token, opener) except ContentSizeExceededError: - return True - return raw.startswith(_PDF_MAGIC_PREFIX) + return PurePosixPath(changed.path).suffix.lower() == ".pdf" + suffix = PurePosixPath(changed.path).suffix.lower() + if suffix == ".png": + return _is_complete_png(raw) + return raw.startswith(BINARY_DOCUMENT_MAGIC[suffix]) + + +def _png_unfilter_row(filtered: bytes, previous: bytes, filter_type: int, bytes_per_pixel: int) -> bytes: + """Reconstruct one PNG scanline for bounded indexed-pixel validation.""" + + reconstructed = bytearray(len(filtered)) + for index, value in enumerate(filtered): + left = reconstructed[index - bytes_per_pixel] if index >= bytes_per_pixel else 0 + above = previous[index] if previous else 0 + upper_left = previous[index - bytes_per_pixel] if previous and index >= bytes_per_pixel else 0 + if filter_type == 0: + predictor = 0 + elif filter_type == 1: + predictor = left + elif filter_type == 2: + predictor = above + elif filter_type == 3: + predictor = (left + above) // 2 + else: + estimate = left + above - upper_left + distances = (abs(estimate - left), abs(estimate - above), abs(estimate - upper_left)) + predictor = (left, above, upper_left)[distances.index(min(distances))] + reconstructed[index] = (value + predictor) & 0xFF + return bytes(reconstructed) + + +def _is_complete_png(raw: bytes) -> bool: + """Validate one bounded PNG including its null- or Adam7-interlaced stream.""" + + if not raw.startswith(PNG_SIGNATURE): + return False + offset = len(PNG_SIGNATURE) + header: tuple[int, int, int, int, int] | None = None + palette_entries = 0 + image_data: list[bytes] = [] + image_data_closed = False + while offset + 12 <= len(raw): + length = int.from_bytes(raw[offset : offset + 4], "big") + chunk_end = offset + 12 + length + if chunk_end > len(raw): + return False + chunk_type = raw[offset + 4 : offset + 8] + chunk_data = raw[offset + 8 : offset + 8 + length] + expected_crc = int.from_bytes(raw[offset + 8 + length : chunk_end], "big") + if ( + any(not (65 <= byte <= 90 or 97 <= byte <= 122) for byte in chunk_type) + or chunk_type[2] & 0x20 + or zlib.crc32(chunk_type + chunk_data) != expected_crc + ): + return False + if header is None: + if chunk_type != b"IHDR" or length != 13 or offset != len(PNG_SIGNATURE): + return False + width = int.from_bytes(chunk_data[0:4], "big") + height = int.from_bytes(chunk_data[4:8], "big") + bit_depth, color_type, compression, filtering, interlace = chunk_data[8:13] + allowed_depths = { + 0: {1, 2, 4, 8, 16}, 2: {8, 16}, 3: {1, 2, 4, 8}, + 4: {8, 16}, 6: {8, 16}, + } + if ( + width == 0 or height == 0 + or bit_depth not in allowed_depths.get(color_type, set()) + or compression != 0 or filtering != 0 or interlace not in {0, 1} + ): + return False + header = (width, height, bit_depth, color_type, interlace) + elif chunk_type == b"IHDR": + return False + elif chunk_type == b"PLTE": + if palette_entries or image_data or length == 0 or length > 768 or length % 3: + return False + _width, _height, bit_depth, color_type, _interlace = header + if color_type == 3 and length // 3 > 1 << bit_depth: + return False + palette_entries = length // 3 + elif chunk_type == b"IDAT": + if image_data_closed: + return False + image_data.append(chunk_data) + elif chunk_type == b"IEND": + if length != 0 or not image_data or chunk_end != len(raw): + return False + width, height, bit_depth, color_type, interlace = header + if (color_type == 3 and not palette_entries) or ( + color_type in {0, 4} and palette_entries + ): + return False + channels = {0: 1, 2: 3, 3: 1, 4: 2, 6: 4}[color_type] + passes = ( + ((0, 0, 8, 8), (4, 0, 8, 8), (0, 4, 4, 8), (2, 0, 4, 4), + (0, 2, 2, 4), (1, 0, 2, 2), (0, 1, 1, 2)) + if interlace else ((0, 0, 1, 1),) + ) + scanlines: list[tuple[int, int, int]] = [] + expected_size = 0 + for x_start, y_start, x_step, y_step in passes: + if width <= x_start or height <= y_start: + continue + pass_width = (width - x_start + x_step - 1) // x_step + pass_height = (height - y_start + y_step - 1) // y_step + row_bytes = (pass_width * channels * bit_depth + 7) // 8 + expected_size += pass_height * (row_bytes + 1) + if expected_size > MAX_RESPONSE_BYTES: + return False + scanlines.append((pass_height, row_bytes, pass_width)) + decoder = zlib.decompressobj() + try: + decoded = decoder.decompress(b"".join(image_data), expected_size + 1) + except zlib.error: + return False + if ( + len(decoded) != expected_size or not decoder.eof + or decoder.unused_data or decoder.unconsumed_tail + ): + return False + decoded_offset = 0 + for row_count, row_bytes, pass_width in scanlines: + previous = b"" + for _ in range(row_count): + filter_type = decoded[decoded_offset] + if filter_type > 4: + return False + filtered = decoded[decoded_offset + 1 : decoded_offset + row_bytes + 1] + if color_type == 3: + reconstructed = _png_unfilter_row(filtered, previous, filter_type, 1) + mask = (1 << bit_depth) - 1 + for pixel in range(pass_width): + bit_offset = pixel * bit_depth + palette_index = ( + reconstructed[bit_offset // 8] + >> (8 - bit_depth - bit_offset % 8) + ) & mask + if palette_index >= palette_entries: + return False + previous = reconstructed + decoded_offset += row_bytes + 1 + return decoded_offset == len(decoded) + elif chunk_type[0] & 0x20 == 0: + return False + elif image_data: + image_data_closed = True + offset = chunk_end + return False def _needs_content_scan(changed: ChangedFile) -> bool: """Return whether a changed final file can carry an active edge runtime. - A claimed binary documentation PDF (``_is_binary_documentation_pdf``) + A claimed binary documentation asset (``_is_binary_documentation_asset``) exempts here on the cheap, offline pre-filter alone; ``evaluate_pull_request`` - never actually relies on that -- it runs ``_pdf_evidence_confirms_binary`` + never actually relies on that -- it runs ``_binary_documentation_evidence_confirms`` for that case before this function is even consulted. """ if changed.status == "removed" or _is_documentation_or_source_fixture(changed.path): return False - if _is_binary_documentation_pdf(changed): + if _is_binary_documentation_asset(changed): return False if not changed.patch_available: return True @@ -499,17 +650,17 @@ def evaluate_pull_request( changed_files = _load_changed_files(api_url.rstrip("/"), repository, pull_request, token, opener) violations: list[Violation] = [] for changed in changed_files: - # A claimed binary documentation PDF gets its own network-verified + # A claimed binary documentation asset gets its own network-verified # check ahead of _needs_content_scan's patch-presence-only signal: # a missing patch does not by itself prove binary content (GitHub # also omits one for an oversized textual diff), so this confirms - # the real %PDF- magic prefix whenever the bytes can be fetched at + # the format's magic prefix whenever the bytes can be fetched at # all, falling back to the path+suffix convention only when the # content genuinely exceeds the Contents API's size ceiling. A # removed file has no head content to fetch at all -- _needs_content_scan # already special-cases this the same way for every other file. - if changed.status != "removed" and _is_binary_documentation_pdf(changed): - if _pdf_evidence_confirms_binary( + if changed.status != "removed" and _is_binary_documentation_asset(changed): + if _binary_documentation_evidence_confirms( changed, api_url=api_url.rstrip("/"), repository=repository, diff --git a/tests/test_pingora_edge_policy.py b/tests/test_pingora_edge_policy.py index 96692b4430..c5d4e9d7a3 100644 --- a/tests/test_pingora_edge_policy.py +++ b/tests/test_pingora_edge_policy.py @@ -7,6 +7,7 @@ import inspect import re import sys +import zlib from io import BytesIO from pathlib import Path from urllib.error import HTTPError, URLError @@ -383,10 +384,195 @@ def opener(url: str, _token: str) -> object: assert result == () +def test_evaluate_pull_request_exempts_a_real_documentation_png() -> None: + """A screenshot is verified by PNG magic instead of decoded as UTF-8.""" + + def opener(url: str, _token: str) -> object: + if "/pulls/15/files" in url: + return [{"filename": "docs/screenshots/dashboard.png", "status": "added"}] + assert "/contents/docs/screenshots/dashboard.png" in url + raw = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" + ) + return { + "type": "file", + "encoding": "base64", + "size": len(raw), + "content": base64.b64encode(raw).decode("ascii"), + } + + assert policy.evaluate_pull_request( + api_url="https://api.github.test", + repository="ContextualWisdomLab/example", + pull_request=15, + head_sha="a" * 40, + event_action="opened", + token="token", + opener=opener, + ) == () + + +def test_evaluate_pull_request_rejects_a_fake_documentation_png() -> None: + """A PNG suffix without PNG magic remains runtime-content evidence.""" + + def opener(url: str, _token: str) -> object: + if "/pulls/16/files" in url: + return [{"filename": "docs/screenshots/fake.png", "status": "added"}] + return encoded_file("cat /etc/nginx/nginx.conf\n") + + result = policy.evaluate_pull_request( + api_url="https://api.github.test", + repository="ContextualWisdomLab/example", + pull_request=16, + head_sha="b" * 40, + event_action="opened", + token="token", + opener=opener, + ) + assert [item.rule for item in result] == ["nginx_runtime_path"] + + +def test_evaluate_pull_request_rejects_png_with_appended_runtime_text() -> None: + """A valid image prefix cannot hide bytes appended after the IEND chunk.""" + + image = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" + ) + + def opener(url: str, _token: str) -> object: + if "/pulls/17/files" in url: + return [{"filename": "docs/screenshots/forged.png", "status": "added"}] + raw = image + b"\ncat /etc/nginx/nginx.conf\n" + return { + "type": "file", "encoding": "base64", "size": len(raw), + "content": base64.b64encode(raw).decode("ascii"), + } + + with pytest.raises(policy.PolicyError, match="not valid UTF-8"): + policy.evaluate_pull_request( + api_url="https://api.github.test", + repository="ContextualWisdomLab/example", + pull_request=17, + head_sha="c" * 40, + event_action="opened", + token="token", + opener=opener, + ) + + +def test_png_structure_validation_fails_closed_on_malformed_chunks() -> None: + """Every malformed PNG boundary returns false without parsing past bounds.""" + + def chunk(kind: bytes, data: bytes) -> bytes: + payload = kind + data + return len(data).to_bytes(4, "big") + payload + zlib.crc32(payload).to_bytes(4, "big") + + signature = policy.PNG_SIGNATURE + header = chunk(b"IHDR", b"\0" * 13) + assert not policy._is_complete_png(b"not-png") + assert not policy._is_complete_png(signature) + assert not policy._is_complete_png( + signature + (99).to_bytes(4, "big") + b"IHDR" + b"\0" * 4 + ) + assert not policy._is_complete_png(signature + header[:-1] + b"\0") + assert not policy._is_complete_png(signature + chunk(b"TEXT", b"")) + assert not policy._is_complete_png(signature + header + chunk(b"IEND", b"")) + assert not policy._is_complete_png(signature + header + chunk(b"TEXT", b"")) + + +def test_png_semantic_validation_fails_closed() -> None: + """CRC-valid chunks still need a valid bounded PNG image stream.""" + + def chunk(kind: bytes, data: bytes) -> bytes: + payload = kind + data + return len(data).to_bytes(4, "big") + payload + zlib.crc32(payload).to_bytes(4, "big") + + def png(header: bytes, *chunks: bytes) -> bytes: + return policy.PNG_SIGNATURE + chunk(b"IHDR", header) + b"".join(chunks) + + def indexed_png( + width: int, + height: int, + bit_depth: int, + palette_entries: int, + decoded: bytes, + *, + interlace: int = 0, + ) -> bytes: + header = width.to_bytes(4, "big") + height.to_bytes(4, "big") + bytes((bit_depth, 3, 0, 0, interlace)) + return png( + header, + chunk(b"PLTE", b"\0\0\0" * palette_entries), + chunk(b"IDAT", zlib.compress(decoded)), + chunk(b"IEND", b""), + ) + + rgba = (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 0)) + indexed = (1).to_bytes(4, "big") * 2 + bytes((8, 3, 0, 0, 0)) + gray = (1).to_bytes(4, "big") * 2 + bytes((8, 0, 0, 0, 0)) + image = chunk(b"IDAT", zlib.compress(b"\0\0\0\0\0")) + end = chunk(b"IEND", b"") + + invalid_headers = ( + b"\0" * 13, + (1).to_bytes(4, "big") * 2 + bytes((4, 2, 0, 0, 0)), + (1).to_bytes(4, "big") * 2 + bytes((8, 6, 1, 0, 0)), + (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 1, 0)), + (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 2)), + ) + assert all(not policy._is_complete_png(png(header, image, end)) for header in invalid_headers) + assert not policy._is_complete_png(png(rgba, chunk(b"IHDR", rgba), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"PLTE", b""), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"PLTE", b"x" * 769), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"PLTE", b"x"), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"1EXt", b""), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"tExt", b""), image, end)) + assert not policy._is_complete_png(png(rgba, chunk(b"ABCD", b""), image, end)) + assert policy._is_complete_png(png(rgba, chunk(b"tEXt", b"x"), image, end)) + assert not policy._is_complete_png(png(rgba, image, chunk(b"tEXt", b"x"), image, end)) + assert not policy._is_complete_png(png(indexed, image, end)) + indexed_one_bit = (1).to_bytes(4, "big") * 2 + bytes((1, 3, 0, 0, 0)) + assert not policy._is_complete_png( + png(indexed_one_bit, chunk(b"PLTE", b"\0" * 9), chunk(b"IDAT", zlib.compress(b"\0\0")), end) + ) + for filter_type in range(5): + second_row = b"\1\0" if filter_type == 0 else b"\1\xff" + assert policy._is_complete_png( + indexed_png(2, 2, 8, 2, bytes((filter_type, 0, 1, filter_type)) + second_row) + ) + assert not policy._is_complete_png(indexed_png(2, 1, 8, 1, b"\0\0\1")) + assert not policy._is_complete_png(indexed_png(2, 2, 8, 2, b"\0\0\1\4\2\xfe")) + assert policy._is_complete_png(indexed_png(2, 1, 1, 2, b"\0\x40")) + assert not policy._is_complete_png(indexed_png(2, 1, 1, 1, b"\0\x40")) + assert policy._is_complete_png(indexed_png(1, 1, 8, 1, b"\0\0", interlace=1)) + assert not policy._is_complete_png(indexed_png(1, 1, 8, 1, b"\0\1", interlace=1)) + assert not policy._is_complete_png(png(gray, chunk(b"PLTE", b"\0\0\0"), chunk(b"IDAT", zlib.compress(b"\0\0")), end)) + assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", b"not-zlib"), end)) + assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", zlib.compress(b"\0")), end)) + assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", zlib.compress(b"\0\0\0\0\0") + b"x"), end)) + assert not policy._is_complete_png(png(rgba, chunk(b"IDAT", zlib.compress(b"\5\0\0\0\0")), end)) + huge = (policy.MAX_RESPONSE_BYTES).to_bytes(4, "big") + (1).to_bytes(4, "big") + bytes((8, 6, 0, 0, 0)) + assert not policy._is_complete_png(png(huge, image, end)) + + adam7 = (8).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 1)) + adam7_scanlines = b"".join( + b"\0" + b"\0" * (pass_width * 4) + for pass_width, pass_height in ((1, 1), (1, 1), (2, 1), (2, 2), (4, 2), (4, 4), (8, 4)) + for _ in range(pass_height) + ) + assert policy._is_complete_png( + png(adam7, chunk(b"IDAT", zlib.compress(adam7_scanlines)), end) + ) + adam7_one_pixel = (1).to_bytes(4, "big") * 2 + bytes((8, 6, 0, 0, 1)) + assert policy._is_complete_png( + png(adam7_one_pixel, chunk(b"IDAT", zlib.compress(b"\0\0\0\0\0")), end) + ) + + def test_evaluate_pull_request_does_not_fetch_a_removed_binary_pdf() -> None: """A removed documentation PDF has no head content to fetch at all. - Regression coverage for Devin Review's finding: _is_binary_documentation_pdf + Regression coverage for Devin Review's finding: _is_binary_documentation_asset does not itself check status, so without an explicit removed-status guard in evaluate_pull_request's own loop, a deleted PDF would try to fetch its (nonexistent) head content and fail evidence collection for every such