1"""Stage an untrusted preview ZIP without letting it replace packaging code."""
2
3import shutil
4import stat
5import sys
6import zipfile
7from pathlib import Path
8
9ROOTS = ("server/dist", "desktop/dist-electron")
10REQUIRED_FILES = {
11 "server/dist/bin.mjs",
12 "server/dist/client/index.html",
13 "desktop/dist-electron/main.cjs",
14}
15# The current bundle is about 32 MiB compressed. Bound extraction on the
16# trusted runner even when the PR replaces the uploader entirely.
17MAX_ARCHIVE_BYTES = 512 * 1024 * 1024
18MAX_EXPANDED_BYTES = 2 * 1024 * 1024 * 1024
19MAX_ENTRIES = 50_000
20
21
22def stage_bundle(archive: Path, destination: Path):
23 if archive.stat().st_size > MAX_ARCHIVE_BYTES:
24 raise ValueError("Preview archive is too large")
25 with zipfile.ZipFile(archive) as bundle:
26 entries = bundle.infolist()
27 if len(entries) > MAX_ENTRIES:
28 raise ValueError("Preview archive has too many entries")
29 if sum(entry.file_size for entry in entries) > MAX_EXPANDED_BYTES:
30 raise ValueError("Expanded preview bundle is too large")
31 seen = set()
32 files = set()
33 for entry in entries:
34 name = entry.filename.removesuffix("/")
35 parts = name.split("/")
36 # Reject ambiguous paths before normalization, including names
37 # that would alias on the macOS signing runner.
38 if (
39 entry.orig_filename != entry.filename
40 or any(part in ("", ".", "..") for part in parts)
41 or any(char in name for char in "\\:")
42 or not name.isascii()
43 or any(ord(char) < 32 or ord(char) == 127 for char in name)
44 ):
45 raise ValueError(f"Unsafe preview path: {entry.filename!r}")
46 allowed = any(name.startswith(root + "/") for root in ROOTS)
47 if entry.is_dir():
48 allowed |= any(root == name or root.startswith(name + "/") for root in ROOTS)
49 if not allowed:
50 raise ValueError(f"Unexpected preview path: {name!r}")
51 kind = stat.S_IFMT(entry.external_attr >> 16)
52 if kind not in (0, stat.S_IFDIR if entry.is_dir() else stat.S_IFREG):
53 raise ValueError(f"Non-regular preview entry: {name!r}")
54 if name.casefold() in seen:
55 raise ValueError(f"Duplicate preview path: {name!r}")
56 seen.add(name.casefold())
57 if not entry.is_dir():
58 files.add(name)
59 if not REQUIRED_FILES <= files:
60 raise ValueError("Preview bundle is missing required entry points")
61 # Validate all names before writing anything. This is a fresh directory
62 # outside the checkout; neither pre-existing links nor trusted files
63 # can be followed or overwritten. ZIP permissions are never restored.
64 destination.mkdir(parents=True, exist_ok=False)
65 for entry in entries:
66 target = destination / entry.filename
67 if entry.is_dir():
68 target.mkdir(parents=True, exist_ok=True)
69 else:
70 target.parent.mkdir(parents=True, exist_ok=True)
71 with bundle.open(entry) as source, target.open("xb") as output:
72 shutil.copyfileobj(source, output)
73
74
75if __name__ == "__main__":
76 stage_bundle(Path(sys.argv[1]), Path(sys.argv[2]))