import argparse import json import os from pathlib import Path import signal import subprocess import tempfile parser = argparse.ArgumentParser(description="Temporarily mutate ZIP extraction, verify tests, and restore source.") parser.add_argument("repo", type=Path) parser.add_argument("--new-only", action="store_true") args = parser.parse_args() repo = args.repo.resolve() source = repo / "src/utils/zip.ts" original = source.read_text() committed = subprocess.check_output(["git", "show", "HEAD:src/utils/zip.ts"], cwd=repo, text=True) if original != committed: raise SystemExit("Refusing to mutate a source file with uncommitted changes") evidence = Path(tempfile.mkdtemp(prefix="zip-mutation-proof-")) env = dict(os.environ, CI="true", NETLIFY_TELEMETRY_DISABLED="1") mutations = [('allow-directory-symlink-writes', "if (relative.split(path.sep).includes('..')) {", 'if (false) {'), ('follow-file-symlinks', 'if (existing.isSymbolicLink()) {', 'if (false) {'), ('swallow-archive-errors', 'reject(err instanceof Error ? err : new Error(String(err)))', 'resolve()'), ('swallow-stream-write-errors', 'await pipeline(readStream, createWriteStream(dest, { mode: procMode }))', 'await pipeline(readStream, createWriteStream(dest, { mode: procMode })).catch(() => {})'), ('drop-executable-mode', 'const procMode = getExtractedMode(mode, isDir) & 0o777', 'const procMode = 0o644'), ('extract-symlinks-as-files', 'if (isSymlink) {', 'if (false) {'), ('append-on-reextraction', 'createWriteStream(dest, { mode: procMode })', "createWriteStream(dest, { mode: procMode, flags: 'a' })"), ('extract-macos-metadata', "if (entry.fileName.startsWith('__MACOSX/')) {", 'if (false) {'), ('reject-empty-archives', 'if (!canceled) resolve()', "if (!canceled) { if (zipfile.entryCount === 0) reject(new Error('empty archive')); else resolve() }"), ('truncate-extracted-bytes', 'await pipeline(readStream, createWriteStream(dest, { mode: procMode }))', 'await pipeline(readStream, createWriteStream(dest, { mode: procMode })); await fs.truncate(dest, 0)'), ('forget-canonical-destination', 'const resolvedDir = await fs.realpath(dir)', 'const resolvedDir = dir'), ('swallow-open-errors', 'const zipfile = await openZip(zipPath, { lazyEntries: true })', 'const zipfile = await openZip(zipPath, { lazyEntries: true }).catch(() => undefined); if (!zipfile) return'), ('leak-archive-on-failure', ' zipfile.close()', ' // deliberately omit cleanup')] results = [] try: for name, before, after in mutations: if original.count(before) != 1: raise RuntimeError("Source no longer matches mutation: " + name) source.write_text(original.replace(before, after)) row = {"mutation": name} suites = [("new", ["tests/unit/utils/zip.test.ts"])] if not args.new_only: suites.insert(0, ("old", ["tests/unit/", "--exclude", "tests/unit/utils/zip.test.ts"])) for suite, paths in suites: command = ["npm", "exec", "vitest", "--", "run", *paths] process = subprocess.Popen(command, cwd=repo, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, start_new_session=True) try: output, _ = process.communicate(timeout=120) except subprocess.TimeoutExpired: os.killpg(process.pid, signal.SIGTERM) output, _ = process.communicate() (evidence / (name + "-" + suite + ".txt")).write_text(output) raise RuntimeError("Mutation timed out: " + name) (evidence / (name + "-" + suite + ".txt")).write_text(output) row[suite] = {"exit": process.returncode, "summary": [line.strip() for line in output.splitlines() if "Tests " in line or "Test Files" in line or "FAIL " in line]} results.append(row) print(json.dumps(row), flush=True) if (not args.new_only and row["old"]["exit"] != 0) or row["new"]["exit"] != 1 or not any(line.startswith("FAIL ") for line in row["new"]["summary"]): raise RuntimeError("Unexpected mutation result: " + name) finally: source.write_text(original) (evidence / "results.json").write_text(json.dumps(results, indent=2)) print(json.dumps({"evidence": str(evidence), "sourceRestored": source.read_text() == original}), flush=True)