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# Bound trusted-runner extraction even when a PR replaces the uploader.
16MAX_ARCHIVE_BYTES = 512 * 1024 * 1024
17MAX_EXPANDED_BYTES = 2 * 1024 * 1024 * 1024
18MAX_ENTRIES = 50_000
19
20
21def stage_bundle(archive: Path, destination: Path):
22 if archive.stat().st_size > MAX_ARCHIVE_BYTES:
23 raise ValueError("Preview archive is too large")
24 with zipfile.ZipFile(archive) as bundle:
25 entries = bundle.infolist()
26 if len(entries) > MAX_ENTRIES:
27 raise ValueError("Preview archive has too many entries")
28 if sum(entry.file_size for entry in entries) > MAX_EXPANDED_BYTES:
29 raise ValueError("Expanded preview bundle is too large")
30 seen = set()
31 files = set()
32 for entry in entries:
33 name = entry.filename.removesuffix("/")
34 parts = name.split("/")
35 # Reject ambiguous paths before normalization, including names
36 # that would alias on the macOS signing runner.
37 if (
38 entry.orig_filename != entry.filename
39 or any(part in ("", ".", "..") for part in parts)
40 or any(char in name for char in "\\:")
41 or not name.isascii()
42 or any(ord(char) < 32 or ord(char) == 127 for char in name)
43 ):
44 raise ValueError(f"Unsafe preview path: {entry.filename!r}")
45 allowed = any(name.startswith(root + "/") for root in ROOTS)
46 if entry.is_dir():
47 allowed |= any(root == name or root.startswith(name + "/") for root in ROOTS)
48 if not allowed:
49 raise ValueError(f"Unexpected preview path: {name!r}")
50 kind = stat.S_IFMT(entry.external_attr >> 16)
51 if kind not in (0, stat.S_IFDIR if entry.is_dir() else stat.S_IFREG):
52 raise ValueError(f"Non-regular preview entry: {name!r}")
53 if name.casefold() in seen:
54 raise ValueError(f"Duplicate preview path: {name!r}")
55 seen.add(name.casefold())
56 if not entry.is_dir():
57 files.add(name)
58 if not REQUIRED_FILES <= files:
59 raise ValueError("Preview bundle is missing required entry points")
60 # Validate before writing into a fresh directory so existing links/files cannot be followed or overwritten.
61 destination.mkdir(parents=True, exist_ok=False)
62 for entry in entries:
63 target = destination / entry.filename
64 if entry.is_dir():
65 target.mkdir(parents=True, exist_ok=True)
66 else:
67 target.parent.mkdir(parents=True, exist_ok=True)
68 with bundle.open(entry) as source, target.open("xb") as output:
69 shutil.copyfileobj(source, output)
70
71
72if __name__ == "__main__":
73 stage_bundle(Path(sys.argv[1]), Path(sys.argv[2]))