File size: 2,704 Bytes
a6a5d8e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#!/usr/bin/env python3
"""Build or verify deterministic SHA-256 manifests for payload directories."""

from __future__ import annotations

import argparse
import hashlib
import json
from pathlib import Path


def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def collect_files(root: Path, output: Path) -> list[dict[str, object]]:
    files: list[dict[str, object]] = []
    output = output.resolve()

    for path in sorted(root.rglob("*")):
        if not path.is_file():
            continue
        if path.resolve() == output:
            continue

        rel = path.relative_to(root).as_posix()
        files.append(
            {
                "path": rel,
                "size": path.stat().st_size,
                "sha256": sha256_file(path),
            }
        )

    return files


def build_manifest(root: Path, output: Path) -> dict[str, object]:
    files = collect_files(root, output)
    aggregate_input = "\n".join(
        f"{entry['path']}\0{entry['size']}\0{entry['sha256']}" for entry in files
    ).encode("utf-8")

    return {
        "manifestVersion": 1,
        "payloadRoot": root.name,
        "generator": "scripts/payload_manifest.py",
        "fileCount": len(files),
        "aggregateSha256": hashlib.sha256(aggregate_input).hexdigest(),
        "files": files,
    }


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("payload_dir", type=Path)
    parser.add_argument("--output", required=True, type=Path)
    parser.add_argument("--verify", action="store_true")
    args = parser.parse_args()

    root = args.payload_dir.resolve()
    output = args.output.resolve()
    manifest = build_manifest(root, output)
    serialized = json.dumps(manifest, indent=2, sort_keys=False) + "\n"

    if args.verify:
        existing = output.read_text(encoding="utf-8")
        if existing != serialized:
            rel_output = output.relative_to(Path.cwd()) if output.is_relative_to(Path.cwd()) else output
            print(f"Payload manifest is stale: {rel_output}")
            print(
                "Run: python3 scripts/payload_manifest.py "
                f"{args.payload_dir} --output {args.output}"
            )
            return 1
        print(f"Verified payload manifest: {output.relative_to(Path.cwd())}")
        return 0

    output.write_text(serialized, encoding="utf-8")
    print(f"Wrote payload manifest: {output.relative_to(Path.cwd())}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())