#!/usr/bin/env python3 """Read one bounded UTF-8 regular file from an immutable Git commit.""" import argparse import os from pathlib import PurePosixPath import re import subprocess import sys MAX_BYTES = 512 * 1024 SHA_RE = re.compile(r"^[0-9a-f]{40}$") REGULAR_MODES = {"100644", "100755"} class ReadError(Exception): pass def git(repo: str, *args: str, text: bool = False) -> subprocess.CompletedProcess: env = {"PATH": os.environ.get("PATH", "/usr/bin:/bin"), "LC_ALL": "C"} return subprocess.run( ["git", *args], cwd=repo, env=env, capture_output=True, text=text, check=False, ) def validate_path(value: str) -> str: path = PurePosixPath(value) parts = value.split("/") if ( not value or value.startswith(("/", ":")) or "\\" in value or "\x00" in value or any(part in {"", ".", ".."} for part in parts) or path.is_absolute() ): raise ReadError("invalid repository-relative path") return value def read_blob(repo: str, head_sha: str, path: str) -> str: if not SHA_RE.fullmatch(head_sha): raise ReadError("head SHA must be 40 lowercase hexadecimal characters") path = validate_path(path) commit = git(repo, "cat-file", "-e", f"{head_sha}^{{commit}}") if commit.returncode != 0: raise ReadError("head SHA is not an available commit") listing = git(repo, "--literal-pathspecs", "ls-tree", "-z", head_sha, "--", path) records = [record for record in listing.stdout.split(b"\0") if record] if listing.returncode != 0 or len(records) != 1: raise ReadError("path is not a regular file at the requested commit") metadata, separator, matched_path = records[0].partition(b"\t") fields = metadata.split() if not separator or len(fields) != 3 or matched_path != path.encode("utf-8"): raise ReadError("path is not a regular file at the requested commit") mode, object_type, object_id = (field.decode("ascii") for field in fields) if mode not in REGULAR_MODES or object_type != "blob": raise ReadError("path is not a regular file at the requested commit") size_result = git(repo, "cat-file", "-s", object_id, text=True) if size_result.returncode != 0: raise ReadError("could not inspect file object") size = int(size_result.stdout.strip()) if size > MAX_BYTES: raise ReadError(f"file exceeds {MAX_BYTES} bytes") content = git(repo, "cat-file", "blob", object_id) if content.returncode != 0: raise ReadError("could not read file object") try: return content.stdout.decode("utf-8") except UnicodeDecodeError as exc: raise ReadError("file must be UTF-8 text") from exc def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--repo", required=True) parser.add_argument("--head-sha", required=True) parser.add_argument("--path", required=True) args = parser.parse_args() try: sys.stdout.write(read_blob(args.repo, args.head_sha, args.path)) except (OSError, ReadError, ValueError) as exc: print(f"PR file read failed: {exc}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())