"""Whitelist-only LFS archives; content verification, not a redistribution grant. No source writes, cache-based trust, extraction, or Git operations. Failed packs remain incomplete and cannot be retried into the same output/work directories. File stability checks are observations, not an atomic filesystem snapshot. """ from __future__ import annotations import argparse import gzip import hashlib import io import json import os from pathlib import Path, PurePosixPath import re import stat import subprocess import tarfile import tempfile import threading SOURCE_ROOT = "/data/digital-drosophila" CHUNK_BYTES = 64 * 1024 * 1024 BUFFER = 1024 * 1024 DENIED_DIRS = {".git", ".venv", "venv", "env", "__pycache__", ".cache", "node_modules", ".pytest_cache", ".mypy_cache", "extension-build"} LOG_DIRS = {"logs", "_logs"} BINARY_SUFFIXES = {".npy", ".npz", ".feather", ".parquet", ".gz", ".zst", ".zip", ".tar", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".pdf", ".woff", ".woff2", ".ttf", ".ico", ".so", ".o", ".a", ".bin"} HEX = re.compile(r"^[0-9a-f]{64}$") SECRET_RULES = ( ("private-key", re.compile(r"-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----")), ("github-token", re.compile(r"\b(?:ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b")), ("aws-access-key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")), ("url-user-password", re.compile(r"\b[a-zA-Z][a-zA-Z0-9+.-]*://[^\s/:@]+:[^\s/@]+@")), ("secret-literal", re.compile(r'''(?ix)\b(?:password|passwd|api[_-]?key|secret[_-]?key)\b["']?\s*[:=]\s*(?:["']([^"'\r\n]+)["']|([A-Za-z0-9_+/@.-]{4,}))''')), ) def canonical(value): return (json.dumps(value, sort_keys=True, indent=2, ensure_ascii=False, allow_nan=False) + "\n").encode() def json_bytes(data): def unique(pairs): value = {} for key, item in pairs: if key in value: raise ValueError("Duplicate JSON key") value[key] = item return value return json.loads(data, object_pairs_hook=unique) def json_read(path): return json_bytes(Path(path).read_bytes()) def relpath(value): if not isinstance(value, str) or not value or "\\" in value or "\x00" in value: raise ValueError("Invalid relative path") p = PurePosixPath(value) if p.is_absolute() or any(x in ("", ".", "..") for x in value.split("/")) or ":" in value: raise ValueError("Unsafe relative path") return p.as_posix() def forbidden(relative): parts = PurePosixPath(relative).parts lower = [x.lower() for x in parts] if any(x in DENIED_DIRS or x.startswith(".venv") for x in lower): return "git-environment-or-cache" if any(x.startswith("phase4ar") for x in lower): return "active-phase4ar" if any(x in LOG_DIRS for x in lower) or lower[-1].endswith((".log", ".log.gz")): return "log" name = lower[-1] if (name == ".env" or name.startswith(".env.") or name in {"id_rsa", "id_ed25519", "id_dsa", ".git-credentials", ".netrc"} or any(x in name for x in ("credentials", "secrets")) or name.endswith((".pem", ".key", ".p12", ".pfx"))): return "credential-file" return None def no_links(path): if ".." in Path(path).parts: raise ValueError("Parent path components refused") p = Path(path).absolute() for part in (p, *p.parents): if part.exists() or part.is_symlink(): s = part.lstat() if (stat.S_ISLNK(s.st_mode) or getattr(s, "st_file_attributes", 0) & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)): raise ValueError("Symlink refused: " + str(part)) return p def identity(s): return (s.st_dev, s.st_ino, s.st_mode, s.st_size, s.st_mtime_ns, s.st_ctime_ns) def opened_identity(handle, expected): actual = identity(os.fstat(handle.fileno())) # Some Windows Python builds expose birth time via stat().ctime but change # time via fstat().ctime. Check each clock against itself before/after; never # omit ctime from either endpoint's stability check. Linux uses all fields. same = actual[:-1] == expected[:-1] if os.name == "nt" else actual == expected if not same: raise ValueError("File changed while opening") return actual def sha_file(path): p = no_links(path) before = p.stat() if not stat.S_ISREG(before.st_mode): raise ValueError("Not a regular file: " + str(p)) h = hashlib.sha256() size = 0 with p.open("rb") as f: opened = opened_identity(f, identity(before)) while block := f.read(BUFFER): h.update(block) size += len(block) if identity(os.fstat(f.fileno())) != opened: raise ValueError("File changed while hashing: " + str(p)) if identity(p.lstat()) != identity(before) or size != before.st_size: raise ValueError("File changed while hashing: " + str(p)) return {"sha256": h.hexdigest(), "bytes": size} def write_new(path, data): with Path(path).open("xb") as f: f.write(data) def validate_plan(plan): if not isinstance(plan, dict) or set(plan) - {"schema", "source_root", "groups", "exclude_paths"}: raise ValueError("Unknown plan fields") if plan.get("schema") != "lfs-publication-plan-v1" or plan.get("source_root") != SOURCE_ROOT: raise ValueError("Wrong plan schema/source_root") groups = plan.get("groups") if not isinstance(groups, list) or not groups: raise ValueError("Nonempty groups required") seen, all_paths = set(), [] for g in groups: if not isinstance(g, dict) or set(g) != {"id", "paths", "license", "provenance"}: raise ValueError("Group requires id/paths/license/provenance only") group_id = g["id"] if not isinstance(group_id, str) or not re.fullmatch(r"[a-z0-9][a-z0-9._-]{0,99}", group_id) or group_id in seen: raise ValueError("Invalid or duplicate group id") seen.add(group_id) if not isinstance(g["paths"], list) or not g["paths"] or not g["license"] or not g["provenance"]: raise ValueError("Nonempty paths/license/provenance required") for value in g["paths"]: p = relpath(value) if forbidden(p): raise ValueError("Explicit forbidden path: " + p) if any(p == old or p.startswith(old + "/") or old.startswith(p + "/") for old in all_paths): raise ValueError("Overlapping whitelist paths") all_paths.append(p) excludes = plan.get("exclude_paths", []) if not isinstance(excludes, list): raise ValueError("exclude_paths must be a list") for p in excludes: relpath(p) canonical(plan) def enumerate_group(root, group, excludes): files, omitted = [], [] def visit(p): relative = p.relative_to(root).as_posix() s = p.lstat() if stat.S_ISLNK(s.st_mode): raise ValueError("Symlink refused: " + relative) reason = forbidden(relative) if any(relative == x or relative.startswith(x + "/") for x in excludes): reason = "plan-exclusion" if reason: omitted.append({"path": relative, "reason": reason, "scope": "subtree" if stat.S_ISDIR(s.st_mode) else "file", "bytes": s.st_size if stat.S_ISREG(s.st_mode) else None}) elif stat.S_ISDIR(s.st_mode): for child in sorted(p.iterdir(), key=lambda x: x.name): visit(child) elif stat.S_ISREG(s.st_mode): files.append((relative, identity(s))) else: raise ValueError("Nonregular source refused: " + relative) for relative in group["paths"]: path = no_links(root / relative) visit(path) files.sort(key=lambda x: x[0]) omitted.sort(key=lambda x: x["path"]) if not files: raise ValueError("Empty group refused") return files, omitted def scan_secrets(root, files): for relative, original in files: p = root / relative if original[3] > 2 * 1024 * 1024 or p.suffix.lower() in BINARY_SUFFIXES: continue data = p.read_bytes() if identity(p.lstat()) != original: raise ValueError("Source changed during secret scan: " + relative) try: text = data.decode("utf-8") except UnicodeDecodeError: continue if "\x00" in text: continue # Per-rule ASCII necessary fragments avoid repeated scans of numeric SWC # bodies (their header includes a public URL). Non-ASCII text still uses # every regex because Unicode IGNORECASE has broader equivalences. lower = text.lower() fragments = { "private-key": ("private key",), "github-token": ("ghp_", "github_pat_"), "aws-access-key": ("akia",), "url-user-password": ("://",), "secret-literal": ("password", "passwd", "api", "secret"), } for rule, pattern in SECRET_RULES: if text.isascii() and not any(x in lower for x in fragments[rule]): continue for match in pattern.finditer(text): if rule == "secret-literal": literal = match.group(1) or match.group(2) if literal.lower() in {"none", "null", "false", "true"}: continue raise ValueError("Secret scan refused: " + relative + " [" + rule + "]") def gzip_rows(path, rows): with Path(path).open("xb") as raw: with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as out: for row in rows: out.write((json.dumps(row, sort_keys=True, ensure_ascii=False, allow_nan=False) + "\n").encode()) class HashReader: def __init__(self, handle): self.handle, self.hash, self.size = handle, hashlib.sha256(), 0 def read(self, count=-1): data = self.handle.read(count) self.hash.update(data) self.size += len(data) return data def zstd_command(decompress=False, threads=2): return ["zstd", "-q", "-d", "-c"] if decompress else ["zstd", "-q", "-3", "-T" + str(threads), "-c"] def make_archive(root, files, destination, threads): rows = [] with destination.open("xb") as output, tempfile.TemporaryFile() as errors: proc = subprocess.Popen(zstd_command(threads=threads), stdin=subprocess.PIPE, stdout=output, stderr=errors) try: with tarfile.open(fileobj=proc.stdin, mode="w|", format=tarfile.PAX_FORMAT) as archive: for relative, original in files: p = no_links(root / relative) if identity(p.lstat()) != original: raise ValueError("Source changed before archive: " + relative) with p.open("rb") as source: opened = opened_identity(source, original) reader = HashReader(source) info = tarfile.TarInfo(relative) info.size, info.mode, info.mtime = original[3], stat.S_IMODE(original[2]) & 0o777, 0 archive.addfile(info, reader) if (reader.size != original[3] or identity(os.fstat(source.fileno())) != opened or identity(p.lstat()) != original): raise ValueError("Source changed during archive: " + relative) rows.append({"path": relative, "bytes": reader.size, "sha256": reader.hash.hexdigest()}) proc.stdin.close() if proc.wait() != 0: raise RuntimeError("zstd compression failed; no complete catalog published") except BaseException: proc.kill() proc.wait() raise finally: proc.stdin.close() return rows def split_archive(archive, group_dir): chunks, full = [], hashlib.sha256() with archive.open("rb") as source: index = 0 while first := source.read(min(BUFFER, CHUNK_BYTES)): name = "payload-%06d.bin" % index h, size = hashlib.sha256(), 0 with (group_dir / name).open("xb") as out: block = first while block: out.write(block) full.update(block) h.update(block) size += len(block) if size == CHUNK_BYTES: break block = source.read(min(BUFFER, CHUNK_BYTES - size)) chunks.append({"path": name, "bytes": size, "sha256": h.hexdigest(), "index": index}) index += 1 if not chunks: raise ValueError("Empty compressed archive") return chunks, full.hexdigest() def pack(plan_path, out, work, *, threads=2): if type(threads) is not int or not 1 <= threads <= 64: raise ValueError("threads must be 1..64") plan_bytes = Path(plan_path).read_bytes() plan = json_bytes(plan_bytes) validate_plan(plan) root, out, work = map(no_links, (Path(plan["source_root"]), Path(out), Path(work))) for a, b in ((root, out), (root, work), (out, work)): if a == b or a in b.parents or b in a.parents: raise ValueError("source, out and work must be disjoint trees") if out.exists() or work.exists(): raise FileExistsError("out/work must both be new exclusive paths") out.mkdir(parents=True, exist_ok=False) work.mkdir(parents=True, exist_ok=False) write_new(out / "plan.json", plan_bytes) plan_sha = hashlib.sha256(plan_bytes).hexdigest() results = [] for group in plan["groups"]: files, exclusions = enumerate_group(root, group, plan.get("exclude_paths", [])) scan_secrets(root, files) group_dir = out / group["id"] group_dir.mkdir() archive = work / (group["id"] + ".tar.zst") members = make_archive(root, files, archive, threads) # A second metadata enumeration catches file additions/removals during pack. after_files, after_exclusions = enumerate_group(root, group, plan.get("exclude_paths", [])) if after_files != files or after_exclusions != exclusions: raise ValueError("Source inventory changed during pack") gzip_rows(group_dir / "members.jsonl.gz", members) gzip_rows(group_dir / "exclusions.jsonl.gz", exclusions) chunks, combined = split_archive(archive, group_dir) catalog = dict(schema="lfs-publication-group-v1", group_id=group["id"], source_root=str(root), source_paths=group["paths"], source_files=len(members), source_bytes=sum(x["bytes"] for x in members), license=group["license"], provenance=group["provenance"], plan_sha256=plan_sha, archive_format="tar.pax+zstd", archive_sha256=combined, archive_bytes=sum(x["bytes"] for x in chunks), chunk_bytes=CHUNK_BYTES, chunks=chunks, members=sha_file(group_dir / "members.jsonl.gz"), exclusions=sha_file(group_dir / "exclusions.jsonl.gz"), exclusion_records=len(exclusions), verification_scope="content snapshot, not a license grant or nested-archive secret audit", secret_scan="UTF-8 files <=2 MiB, known binary suffixes excluded; fail closed on matching rules") write_new(group_dir / "catalog.json", canonical(catalog)) result = {"group": group["id"], "files": len(members), "source_bytes": catalog["source_bytes"], "archive_bytes": catalog["archive_bytes"], "chunks": len(chunks), "catalog_sha256": sha_file(group_dir / "catalog.json")["sha256"]} results.append(result) print(json.dumps({"group_complete": result}), flush=True) summary = {"schema": "lfs-publication-pack-v1", "complete": True, "plan_sha256": plan_sha, "groups": results, "source_files": sum(x["files"] for x in results), "source_bytes": sum(x["source_bytes"] for x in results)} write_new(out / "pack-summary.json", canonical(summary)) return summary def check_digest_row(row): if not isinstance(row, dict) or type(row.get("bytes")) is not int or row["bytes"] < 0 or not HEX.fullmatch(str(row.get("sha256", ""))): raise ValueError("Invalid SHA/size metadata") def iter_members(path): previous = None with gzip.open(path, "rt", encoding="utf-8") as inp: for line in inp: row = json.loads(line) if set(row) != {"path", "bytes", "sha256"}: raise ValueError("Invalid member row") check_digest_row(row) p = relpath(row["path"]) if forbidden(p) or (previous is not None and p <= previous): raise ValueError("Forbidden/duplicate/unsorted member: " + p) previous = p yield row def verify(group_dir): group_dir = no_links(group_dir) controls_before = {name: sha_file(group_dir / name) for name in ("catalog.json", "members.jsonl.gz", "exclusions.jsonl.gz")} catalog = json_read(group_dir / "catalog.json") if catalog.get("schema") != "lfs-publication-group-v1" or catalog.get("chunk_bytes") != CHUNK_BYTES or catalog.get("archive_format") != "tar.pax+zstd": raise ValueError("Wrong catalog schema/format/chunk size") chunks = catalog.get("chunks") if not isinstance(chunks, list) or not chunks: raise ValueError("Missing payload chunks") expected = {"catalog.json", "members.jsonl.gz", "exclusions.jsonl.gz"} for i, row in enumerate(chunks): check_digest_row(row) if set(row) != {"path", "bytes", "sha256", "index"} or type(row["index"]) is not int or row["index"] != i or row["path"] != "payload-%06d.bin" % i: raise ValueError("Wrong chunk name/order") if not 0 < row["bytes"] <= CHUNK_BYTES or (i < len(chunks) - 1 and row["bytes"] != CHUNK_BYTES): raise ValueError("Wrong chunk size") expected.add(row["path"]) if {p.name for p in group_dir.iterdir()} != expected: raise ValueError("Extra/missing downloaded files") for p in group_dir.iterdir(): no_links(p) if not p.is_file(): raise ValueError("Nonfile downloaded entry") for name, key in (("members.jsonl.gz", "members"), ("exclusions.jsonl.gz", "exclusions")): check_digest_row(catalog[key]) if controls_before[name] != catalog[key]: raise ValueError("Metadata digest mismatch: " + name) with gzip.open(group_dir / "exclusions.jsonl.gz", "rt", encoding="utf-8") as source: omitted = list(map(json.loads, source)) if len(omitted) != catalog.get("exclusion_records"): raise ValueError("Exclusion inventory mismatch") for row in omitted: relpath(row["path"]) errors, combined, compressed = [], hashlib.sha256(), [0] with tempfile.TemporaryFile() as diagnostics: proc = subprocess.Popen(zstd_command(decompress=True), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=diagnostics) def feed(): try: for row in chunks: p = group_dir / row["path"] before = identity(p.lstat()) h, size = hashlib.sha256(), 0 with p.open("rb") as source: opened = opened_identity(source, before) while data := source.read(BUFFER): h.update(data) combined.update(data) size += len(data) compressed[0] += len(data) proc.stdin.write(data) if identity(os.fstat(source.fileno())) != opened or identity(p.lstat()) != before: raise ValueError("Chunk changed during read") if size != row["bytes"] or h.hexdigest() != row["sha256"]: raise ValueError("Payload digest mismatch: " + row["path"]) except BaseException as exc: errors.append(exc) finally: try: proc.stdin.close() except OSError: pass producer = threading.Thread(target=feed, daemon=True) producer.start() count, total = 0, 0 expected_rows = iter(iter_members(group_dir / "members.jsonl.gz")) try: with tarfile.open(fileobj=proc.stdout, mode="r|") as archive: for member in archive: row = next(expected_rows, None) if not member.isreg() or member.linkname or member.sparse is not None: raise ValueError("Nonregular/sparse/link tar member refused") path = relpath(member.name) if forbidden(path) or row is None or path != row["path"] or member.size != row["bytes"]: raise ValueError("Tar member set/size/path mismatch: " + path) reader = HashReader(archive.extractfile(member)) while reader.read(BUFFER): pass if reader.size != row["bytes"] or reader.hash.hexdigest() != row["sha256"]: raise ValueError("Tar member content mismatch: " + path) count += 1 total += reader.size # tarfile stops at the first end marker; reject a hidden second # archive/nonzero trailer, including bytes in its read-ahead buf. while trailer := archive.fileobj.read(BUFFER): if any(trailer): raise ValueError("Nonzero data after tar end marker") if next(expected_rows, None) is not None: raise ValueError("Missing tar members") # Drain to validate zstd's complete stream/checksum and finish the feeder. while proc.stdout.read(BUFFER): pass producer.join() code = proc.wait() if errors: raise ValueError("Payload streaming failed: " + str(errors[0])) if code != 0: raise ValueError("zstd decompression failed") except BaseException: proc.kill() proc.wait() producer.join(timeout=10) raise finally: proc.stdout.close() expected_rows.close() if (count != catalog.get("source_files") or total != catalog.get("source_bytes") or compressed[0] != catalog.get("archive_bytes") or combined.hexdigest() != catalog.get("archive_sha256")): raise ValueError("Final inventory/archive digest mismatch") for name, before in controls_before.items(): if sha_file(group_dir / name) != before: raise ValueError("Downloaded control changed during verification: " + name) return {"schema": "lfs-publication-verify-v1", "pass_all": True, "group": catalog["group_id"], "source_files": count, "source_bytes": total, "archive_bytes": compressed[0], "archive_sha256": combined.hexdigest(), "catalog_sha256": controls_before["catalog.json"]["sha256"], "scope": "downloaded payload bytes and streamed members; no extraction, no LFS-cache trust; catalog authenticity is caller responsibility"} def main(): parser = argparse.ArgumentParser(description=__doc__) sub = parser.add_subparsers(dest="command", required=True) command = sub.add_parser("pack") command.add_argument("--plan", required=True) command.add_argument("--out", required=True) command.add_argument("--work", required=True) command.add_argument("--threads", type=int, default=2) command = sub.add_parser("verify") command.add_argument("--group-dir", required=True) args = parser.parse_args() result = (pack(args.plan, args.out, args.work, threads=args.threads) if args.command == "pack" else verify(args.group_dir)) print(json.dumps(result, sort_keys=True, ensure_ascii=False, allow_nan=False), flush=True) if __name__ == "__main__": main()