File size: 3,882 Bytes
4be6a52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
"""Source identity for exact Git checkouts and published, hash-verified source bundles."""

import hashlib
import json
import re
import subprocess
from pathlib import Path
from typing import Any

SOURCE_MANIFEST = "source-manifest.json"


def file_sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as stream:
        while chunk := stream.read(1_048_576):
            digest.update(chunk)
    return digest.hexdigest()


def source_identity(root: Path) -> dict[str, Any]:
    """Never mistake an enclosing, unrelated Git repository for this source tree.

    Published manifests identify original source and verify matching local bytes;
    they are provenance records, not signed proof of the claimed commit's authorship.
    """
    root = root.resolve()
    try:
        top = subprocess.check_output(
            ["git", "rev-parse", "--show-toplevel"], cwd=root, text=True, stderr=subprocess.DEVNULL
        ).strip()
        if Path(top).resolve() == root:
            commit = subprocess.check_output(
                ["git", "rev-parse", "HEAD"], cwd=root, text=True, stderr=subprocess.DEVNULL
            ).strip()
            if not re.fullmatch(r"[0-9a-f]{40}", commit):
                raise ValueError("source requires a full 40-character Git commit")
            dirty = bool(
                subprocess.check_output(
                    ["git", "status", "--porcelain"],
                    cwd=root,
                    text=True,
                    stderr=subprocess.DEVNULL,
                ).strip()
            )
            return {
                "source_commit": commit,
                "source_dirty": dirty,
                "source_identity_method": "exact-git-root",
            }
    except (OSError, subprocess.CalledProcessError):
        pass
    path = root / SOURCE_MANIFEST
    if not path.is_file() or path.is_symlink():
        raise ValueError(
            "no exact Stackcraft Git root or bundled source-manifest.json; "
            "run from the released source directory or an initialized source checkout"
        )
    manifest = json.loads(path.read_text())
    commit = manifest.get("source_commit")
    hashes = manifest.get("source_hashes")
    if (
        type(manifest.get("schema_version")) is not int
        or manifest["schema_version"] != 1
        or not isinstance(commit, str)
        or not re.fullmatch(r"[0-9a-f]{40}", commit)
        or type(manifest.get("source_dirty")) is not bool
        or not isinstance(hashes, dict)
        or not hashes
    ):
        raise ValueError("invalid published source-manifest contract")
    changed = []
    for name, expected in hashes.items():
        relative = Path(name)
        if (
            relative.is_absolute()
            or ".." in relative.parts
            or not relative.parts
            or not isinstance(expected, str)
            or not re.fullmatch(r"[0-9a-f]{64}", expected)
        ):
            raise ValueError("invalid source manifest path or SHA256")
        source = root / relative
        if (
            not source.is_file()
            or source.is_symlink()
            or not source.resolve().is_relative_to(root)
            or file_sha256(source) != expected
        ):
            changed.append(name)
    # Newly added importable files can change behavior even if existing files match.
    for folder in ("src", "scripts", "tests"):
        for source in (root / folder).rglob("*.py"):
            relative = str(source.relative_to(root))
            if relative not in hashes:
                changed.append(relative)
    return {
        "source_commit": commit,
        "source_dirty": manifest["source_dirty"] or bool(changed),
        "source_identity_method": "published-source-manifest",
        "source_manifest_sha256": file_sha256(path),
        "modified_source_files": sorted(set(changed)),
    }