101 lines
3.2 KiB
Python
Executable File
101 lines
3.2 KiB
Python
Executable File
#!/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())
|