| from __future__ import annotations |
|
|
| import csv |
| import hashlib |
| import json |
| from pathlib import Path |
|
|
| import pytest |
| from verify_archive import verify_archive |
|
|
|
|
| def _write_archive(root: Path) -> None: |
| for relative, content in { |
| "images/example.png": b"image", |
| "results/metrics.csv": b"condition,success\npng,1\n", |
| "samples/example.png": b"sample", |
| "weights/model.png": b"weights", |
| }.items(): |
| path = root / relative |
| path.parent.mkdir(parents=True, exist_ok=True) |
| path.write_bytes(content) |
| rows = [] |
| for relative in ("weights/model.png",): |
| path = root / relative |
| rows.append( |
| { |
| "path": relative, |
| "bytes": str(path.stat().st_size), |
| "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), |
| } |
| ) |
| with (root / "weights/manifest.csv").open("w", newline="", encoding="utf-8") as handle: |
| writer = csv.DictWriter(handle, fieldnames=["path", "bytes", "sha256"]) |
| writer.writeheader() |
| writer.writerows(rows) |
| manifest = { |
| "files": [ |
| { |
| "path": relative, |
| "bytes": (root / relative).stat().st_size, |
| "sha256": hashlib.sha256((root / relative).read_bytes()).hexdigest(), |
| } |
| for relative in ( |
| "images/example.png", |
| "results/metrics.csv", |
| "samples/example.png", |
| "weights/manifest.csv", |
| ) |
| ] |
| } |
| (root / "results/archive_manifest.json").write_text(json.dumps(manifest), encoding="utf-8") |
|
|
|
|
| def _refresh_manifest(root: Path) -> None: |
| manifest_path = root / "results/archive_manifest.json" |
| manifest = json.loads(manifest_path.read_text(encoding="utf-8")) |
| manifest.pop("files", None) |
| manifest["artifacts"] = [] |
| for path in sorted(p for directory in ("images", "results", "samples", "weights") |
| for p in (root / directory).rglob("*") if p.is_file() and p.name != "archive_manifest.json"): |
| relative = path.relative_to(root).as_posix() |
| manifest["artifacts"].append({"path": relative, "bytes": path.stat().st_size, |
| "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}) |
| manifest.update({"profile": "test", "prompt_count": 1, "conditions": ["png_baseline", "webp_lossless"]}) |
| manifest_path.write_text(json.dumps(manifest), encoding="utf-8") |
|
|
|
|
| def _add_generation_rows(root: Path) -> None: |
| sample = root / "samples/example.png" |
| rows = [ |
| {"row_type": "generation", "condition": "png_baseline", "prompt_id": "p1", "success": True, |
| "sample_path": "samples/example.png", "sample_bytes": sample.stat().st_size, |
| "sample_hash": hashlib.sha256(sample.read_bytes()).hexdigest(), "sample_sha256": None}, |
| {"row_type": "generation", "condition": "webp_lossless", "prompt_id": "p1", "success": False, |
| "sample_path": None, "sample_bytes": None, "sample_hash": None, "sample_sha256": None}, |
| ] |
| (root / "results/raw_rows.jsonl").write_text("".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8") |
| _refresh_manifest(root) |
|
|
|
|
| def test_verify_archive_accepts_hash_and_containment_valid_archive(tmp_path: Path) -> None: |
| _write_archive(tmp_path) |
| _add_generation_rows(tmp_path) |
|
|
| report = verify_archive(tmp_path) |
|
|
| assert report["checked_files"] == 7 |
| assert report["checked_weight_entries"] == 1 |
| assert report["generation_rows"] == 2 |
| assert report["successful_samples"] == 1 |
|
|
|
|
| def test_verify_archive_rejects_path_escape(tmp_path: Path) -> None: |
| _write_archive(tmp_path) |
| _add_generation_rows(tmp_path) |
| manifest_path = tmp_path / "results/archive_manifest.json" |
| manifest = json.loads(manifest_path.read_text(encoding="utf-8")) |
| manifest["artifacts"].append({"path": "../outside", "bytes": 0, "sha256": ""}) |
| manifest_path.write_text(json.dumps(manifest), encoding="utf-8") |
|
|
| with pytest.raises(ValueError, match="containment"): |
| verify_archive(tmp_path) |
|
|
|
|
| @pytest.mark.parametrize("mutation, message", [ |
| ("missing", "sample"), |
| ("hash", "SHA-256"), |
| ("bytes", "byte count"), |
| ]) |
| def test_verify_archive_rejects_invalid_success_provenance(tmp_path: Path, mutation: str, message: str) -> None: |
| _write_archive(tmp_path) |
| _add_generation_rows(tmp_path) |
| raw = tmp_path / "results/raw_rows.jsonl" |
| row = json.loads(raw.read_text(encoding="utf-8").splitlines()[0]) |
| if mutation == "missing": |
| row["sample_path"] = None |
| elif mutation == "hash": |
| row["sample_hash"] = "0" * 64 |
| else: |
| row["sample_bytes"] += 1 |
| raw.write_text(json.dumps(row) + "\n" + raw.read_text(encoding="utf-8").splitlines()[1] + "\n", encoding="utf-8") |
| _refresh_manifest(tmp_path) |
|
|
| with pytest.raises(ValueError, match=message): |
| verify_archive(tmp_path) |
|
|
|
|
| def test_verify_archive_rejects_failed_row_with_sample(tmp_path: Path) -> None: |
| _write_archive(tmp_path) |
| _add_generation_rows(tmp_path) |
| raw = tmp_path / "results/raw_rows.jsonl" |
| rows = [json.loads(line) for line in raw.read_text(encoding="utf-8").splitlines()] |
| rows[1]["sample_path"] = "samples/example.png" |
| raw.write_text("\n".join(json.dumps(row) for row in rows) + "\n", encoding="utf-8") |
| _refresh_manifest(tmp_path) |
|
|
| with pytest.raises(ValueError, match="failed row"): |
| verify_archive(tmp_path) |
|
|
|
|
| def test_verify_archive_rejects_raw_rows_symlink(tmp_path: Path) -> None: |
| _write_archive(tmp_path) |
| _add_generation_rows(tmp_path) |
| raw = tmp_path / "results/raw_rows.jsonl" |
| raw.unlink() |
| raw.symlink_to(tmp_path / "outside.jsonl") |
| (tmp_path / "outside.jsonl").write_text("", encoding="utf-8") |
|
|
| with pytest.raises(ValueError, match="symlink"): |
| verify_archive(tmp_path) |
|
|
|
|
| def test_verify_archive_rejects_duplicate_manifest_path(tmp_path: Path) -> None: |
| _write_archive(tmp_path) |
| _add_generation_rows(tmp_path) |
| manifest_path = tmp_path / "results/archive_manifest.json" |
| manifest = json.loads(manifest_path.read_text(encoding="utf-8")) |
| manifest["artifacts"].append(manifest["artifacts"][0]) |
| manifest_path.write_text(json.dumps(manifest), encoding="utf-8") |
|
|
| with pytest.raises(ValueError, match="duplicate archive path"): |
| verify_archive(tmp_path) |
|
|
|
|
| def test_verify_archive_rejects_listed_hash_tamper(tmp_path: Path) -> None: |
| _write_archive(tmp_path) |
| _add_generation_rows(tmp_path) |
| path = tmp_path / "samples/example.png" |
| path.write_bytes(b"tampeR") |
|
|
| with pytest.raises(ValueError, match="SHA-256"): |
| verify_archive(tmp_path) |
|
|
|
|
| def test_verify_archive_rejects_archived_tree_symlink_and_unlisted_file(tmp_path: Path) -> None: |
| _write_archive(tmp_path) |
| _add_generation_rows(tmp_path) |
| extra = tmp_path / "samples/unlisted.bin" |
| extra.write_bytes(b"extra") |
| with pytest.raises(ValueError, match="unlisted"): |
| verify_archive(tmp_path) |
| extra.unlink() |
| extra.symlink_to(tmp_path / "weights/model.png") |
|
|
| with pytest.raises(ValueError, match="symlink"): |
| verify_archive(tmp_path) |
|
|