from __future__ import annotations import asyncio import contextlib import gzip import io import json import random import re import struct import threading import time import unittest import zipfile from pathlib import Path from types import SimpleNamespace from unittest.mock import AsyncMock, patch from urllib.parse import quote IMPORT_ERROR = None try: import app import httpx except ModuleNotFoundError as exc: app = None IMPORT_ERROR = exc from tests.test_async_proxy import FakeResponse, FakeSession SOURCE = "https://huggingface.co/datasets/VoiceOfML/Test/resolve/main/book29.txt" OBJECT = "objects/aa/" + "a" * 64 BUCKET_PATH = OBJECT + "/" + "b" * 16 + "/page-manifest.json" def make_zip(files=None, comment=b"", zip64=False): output = io.BytesIO() with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as archive: for name, content in (files if files is not None else {"image.svg": b""}).items(): archive.writestr(name, content) archive.comment = comment raw = output.getvalue() if not zip64: return raw offset = len(raw) - 22 - len(comment) end = list(struct.unpack("<4s4H2LH", raw[offset:offset + 22])) record64 = struct.pack("<4sQ2H2L4Q", b"PK\x06\x06", 44, 45, 45, 0, 0, end[4], end[4], end[5], end[6]) locator = struct.pack("<4sLQL", b"PK\x06\x07", 0, offset, 1) end[3:7] = [0xffff, 0xffff, 0xffffffff, 0xffffffff] return raw[:offset] + record64 + locator + struct.pack("<4s4H2LH", *end) + comment @unittest.skipIf(app is None, f"backend dependencies unavailable: {IMPORT_ERROR}") class ReaderArchiveValidationTests(unittest.TestCase): def test_canonical_paths_preserve_character_encoder_semantics(self): prefix = "https://huggingface.co/datasets/VoiceOfML/Test/resolve/main/" def reference(value): def replace(match): token = match.group() if re.fullmatch(r"%[0-9a-fA-F]{2}", token): char = chr(int(token[1:], 16)) return char if re.fullmatch(r"[A-Za-z0-9._~-]", char) else token.upper() return quote(token, safe="") return prefix + re.sub(r"%[0-9a-fA-F]{2}|[^A-Za-z0-9._~/-]", replace, value) rng = random.Random(20260927) tokens = [chr(i) for i in range(128) if chr(i) not in "?#"] tokens += ["\u4e66", "\u00e9", "\U0001f600", "%2f", "%25", "%41", "%7e", "%G0", "%"] paths = ["%" + first + second for i in range(256) for first in {f"{i:02x}"[0], f"{i:02X}"[0]} for second in {f"{i:02x}"[1], f"{i:02X}"[1]}] paths += ["".join(rng.choices(tokens, k=40)) for _ in range(256)] for path in paths: self.assertEqual(app.canonical_reader_source_url(prefix + path), reference(path)) for value in ("https://example.com/%ab", prefix + "a?raw=%ab#fragment"): self.assertEqual(app.canonical_reader_source_url(value), value) def test_base36_independent_fixed_vectors(self): for value, expected in ((0, "0"), (35, "z"), (36, "10"), (72, "20"), (1296, "100"), (2**64 - 1, "3w5e11264sgsf")): with self.subTest(value=value): self.assertEqual(app._base36(value), expected) self.assertEqual(app.reader_short_id(SOURCE), "0ym7e7wbcxau0") vectors = json.loads(Path(__file__).with_name("reader-contract-vectors.json").read_text()) for value, expected in vectors["ids"]: self.assertEqual(app.reader_short_id(value), expected) prefix = "https://huggingface.co/datasets/VoiceOfML/VOMEBOOK/resolve/main/" for value, expected in vectors["canonicalPaths"]: self.assertEqual(app.canonical_reader_source_url(prefix + value), prefix + expected) self.assertEqual(app.canonical_reader_source_url(prefix + expected), prefix + expected) self.assertEqual(app.reader_short_id(prefix + value), app.reader_short_id(prefix + expected)) self.assertEqual(app.reader_short_id(prefix + vectors["reportedBook"]["path"]), vectors["reportedBook"]["id"]) for record, expected in vectors["ocr"]: self.assertEqual(app.build_txt_relative_path(record), expected) def test_valid_standard_zip64_comments_and_prepended_data(self): for zip64 in (False, True): for comment in (b"", b"archive comment"): for prefix in (b"", b"self-extracting prefix"): with self.subTest(zip64=zip64, comment=comment, prefix=prefix): raw = prefix + make_zip(comment=comment, zip64=zip64) app.validate_reader_archive(raw) self.assertEqual(app.read_reader_archive_member(raw, "image.svg"), b"") app.validate_reader_archive(make_zip(files={})) def test_gzip_wrapper_has_bounded_expansion(self): raw = make_zip() self.assertEqual(app.prepare_reader_archive(gzip.compress(raw)), raw) with patch.object(app, "READER_RESOURCE_MAX_ARCHIVE_BYTES", len(raw) - 1): with self.assertRaises(app.ReaderArchiveLimitError): app.prepare_reader_archive(gzip.compress(raw)) with self.assertRaises(app.ReaderArchiveInvalidError): app.prepare_reader_archive(b"\x1f\x8btruncated") def test_directory_limits_checked_before_zipfile_construction(self): for zip64 in (False, True): raw = make_zip(zip64=zip64) for limit in ("READER_ARCHIVE_MAX_CENTRAL_DIRECTORY_BYTES", "READER_ARCHIVE_MAX_ENTRIES"): with self.subTest(zip64=zip64, limit=limit), patch.object(app, limit, 0), patch.object(app.zipfile, "ZipFile") as parser: with self.assertRaises(app.ReaderArchiveLimitError): app.validate_reader_archive(raw) parser.assert_not_called() def test_malformed_end_records_and_multidisk_archives(self): raw = make_zip() variants = [raw[:-1], b"PKnot-a-zip"] for offset, fmt, value in ((4, "H", 1), (10, "H", 2), (16, "L", len(raw) + 1), (20, "H", 8)): bad = bytearray(raw) struct.pack_into("<" + fmt, bad, len(raw) - 22 + offset, value) variants.append(bytes(bad)) raw64 = make_zip(zip64=True) bad64 = bytearray(raw64) struct.pack_into("