Files
craft-libs/scripts/fetch-crw-samples.py
T
RWoldbergandClaude Fable 5.1 a0c8a08020 craft-crw: Canon CRW (CIFF) raw decoder, moved from LightCraft with its clean-room record
The clean-room Canon CRW decoder written in LightCraft (crates/raw/src/vendor/crw.rs,
crw_tables.rs, branch clean-room-crw-format-decode, revision 70c755e4) becomes the shared
crate craft-crw: the CIFF heap container, the entropy coding derived by black-box analysis
(frozen code tables for selectors 0-2), sensor black and white levels, as-shot white
balance, orientation and the make/model/time records. Public types are the crate's own
(Error, Limits, Cfa, Rect, BlackLevel, Header, Image); the capture time is handed on as
Unix seconds so no calendar code lives here; LightCraft's shared plateau estimate is
copied as saturation_plateau. Algorithm, tables and thresholds are unchanged.

Tests: the ported synthetic round-trip, table-structure and hostile-input tests; a
proptest mutation sweep (tests/robust.rs); and tests/samples.rs, which pins the eleven
CC0 raw.pixls.us sample files (tests/samples.json: URL, SHA-256, licence) to the SHA-256
of the verified decode plus levels, white balance and metadata, so the port is proven
byte-identical to the LightCraft decode without redistributing the reference TIFFs.
scripts/fetch-crw-samples.py downloads the files outside the checkout; CRAFT_CRW_SAMPLES
and CRAFT_CRW_ORACLE point the tests at them. The crate builds for wasm32.

The whole derivation record (starting assumptions, plan with the maintainer's decisions,
source log, container and payload notes, derivation log, frozen format, verification,
colour/levels notes, private-corpus and chart notes, scripts, logs and the frozen table
JSON) moves verbatim to crates/crw/clean-room/ with a README that indexes it and maps the
LightCraft paths it cites; tools/gen_tables.py regenerates src/tables.rs from the JSON,
byte for byte.

Repository: .github/workflows/crw.yml (fmt, clippy, tests on Linux, Windows and macOS;
wasm check; the sample-file job with cached downloads), README and ATTRIBUTION rows,
NOTICE source provenance, docs/clean-room.md (the clean-room standard a format crate
follows here), and AGENTS.md expanded with the start-of-session steps, the new-crate
checklist, clean-room, assets and test rules; CLAUDE.md points at it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-26 08:20:01 -06:00

94 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Fetch the pinned CC0 Canon CRW sample files for craft-crw's tests, outside the checkout.
The manifest is crates/crw/tests/samples.json (name, raw.pixls.us URL, SHA-256, licence). Files
already present with the right hash are kept. Then run the tests with
`CRAFT_CRW_SAMPLES=<destination> cargo test -p craft-crw --release --test samples`.
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
MAX_BYTES = 16 * 1024 * 1024
class _HttpsOnlyRedirectHandler(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
if urllib.parse.urlsplit(newurl).scheme != "https":
raise ValueError("refusing to follow a non-HTTPS redirect")
return super().redirect_request(req, fp, code, msg, headers, newurl)
urllib.request.install_opener(urllib.request.build_opener(_HttpsOnlyRedirectHandler))
def fetch(url):
if urllib.parse.urlsplit(url).scheme != "https":
raise ValueError("refusing to fetch a non-HTTPS URL")
for attempt in range(3):
try:
with urllib.request.urlopen(url, timeout=120) as response:
return response.read(MAX_BYTES + 1)
except urllib.error.HTTPError as error:
if error.code not in (408, 429) and not 500 <= error.code <= 599:
raise
if attempt == 2:
raise
error.close()
except (urllib.error.URLError, TimeoutError, ConnectionError):
if attempt == 2:
raise
time.sleep(attempt + 1)
raise RuntimeError("sample download retries exhausted")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("destination", type=Path)
args = parser.parse_args()
root = Path(__file__).resolve().parent.parent
destination = args.destination.resolve()
if destination == root or root in destination.parents:
parser.error("sample files must be stored outside the checkout (they are never committed)")
samples = json.loads((root / "crates/crw/tests/samples.json").read_text(encoding="utf-8"))
destination.mkdir(parents=True, exist_ok=True)
for sample in samples:
name, url, digest, licence = sample["name"], sample["source"], sample["sha256"], sample["license"]
if Path(name).name != name or name in ("", ".", "..") or not url.startswith("https://"):
raise ValueError("invalid sample name or source")
if licence != "CC0-1.0":
raise ValueError(f"{name}: only CC0 samples are fetched (manifest says {licence})")
path = (destination / name).resolve()
if path.parent != destination:
raise ValueError(f"sample path escapes destination: {name}")
if path.is_file() and hashlib.sha256(path.read_bytes()).hexdigest() == digest:
print(f"Verified {name}")
continue
data = fetch(url)
if len(data) > MAX_BYTES or hashlib.sha256(data).hexdigest() != digest:
raise ValueError(f"sample size or SHA-256 mismatch: {name}")
fd, tmp = tempfile.mkstemp(dir=destination, prefix=f".{name}.", suffix=".tmp")
try:
with os.fdopen(fd, "wb") as handle:
handle.write(data)
os.replace(tmp, destination / name)
except BaseException:
try:
os.unlink(tmp)
except OSError:
pass
raise
print(f"Fetched and verified {name}")
if __name__ == "__main__":
main()