Artifact 20358761143cef2734816a1aec13c51bdb5b07cc55350a187c25ca4f120525ac:
- Executable file
scripts/unzip-preserving-modes.py
— part of check-in
[9033c13f21]
at
2026-09-17 17:04:25
on branch trunk
— Remove issue-tracker numbers from source and documents
The tracker is disposable and does not live with the source, so a number is a dangling pointer. Every zit#NN and #NN reference in code comments, test names, help text, smoke labels, identifiers, release notes and errata is replaced by enough context to stand alone, or by a short plain statement of the problem. No behaviour changed. (user: dmestas, size: 3048) [annotate] [blame] [check-ins using]
#!/usr/bin/env python3 """Extract a zip, restoring Unix modes and symlinks. `ZipFile.extractall` deliberately drops both: it creates every member as a regular file with default permissions, so an archive that correctly marked `configure` executable and `link.c` a symlink would unpack indistinguishably from one that got both wrong. That is precisely the defect the archive differential exists to catch, so the extraction has to honour them. Both live in the central directory's `external_attr`, high 16 bits, and count only when `create_system` says Unix (3). The mapping applied here is the one a Unix `unzip` applies; doing it explicitly rather than shelling out keeps the battery from depending on an `unzip` binary the runners do not all carry. Usage: unzip-preserving-modes.py ARCHIVE.zip DEST-DIR """ import os import stat import sys import zipfile # `create_system` value for Unix, per APPNOTE 4.4.2.2. Any other value means # the archive was written by a system whose attributes are not st_mode, and # the high bits must not be read as one. CREATE_SYSTEM_UNIX = 3 def unix_mode(info): """The member's st_mode, or None when the archive does not claim one.""" if info.create_system != CREATE_SYSTEM_UNIX: return None mode = info.external_attr >> 16 return mode if mode else None def main(argv): if len(argv) != 3: raise SystemExit("usage: unzip-preserving-modes.py ARCHIVE.zip DEST-DIR") archive, dest = argv[1], argv[2] os.makedirs(dest, exist_ok=True) dest_real = os.path.realpath(dest) with zipfile.ZipFile(archive) as zf: for info in zf.infolist(): # Refuse a member that would land outside DEST. Nothing this # harness archives can do that, but an extractor that trusts a # member name is the classic zip-slip, and a test helper is not # where that should be learned the hard way. target = os.path.realpath(os.path.join(dest, info.filename)) if target != dest_real and not target.startswith(dest_real + os.sep): raise SystemExit(f"member escapes the destination: {info.filename}") if info.is_dir(): os.makedirs(target, exist_ok=True) continue os.makedirs(os.path.dirname(target), exist_ok=True) mode = unix_mode(info) data = zf.read(info) # A symlink's CONTENT is its target path. Written as a regular file # it becomes a small text file holding something like `../lib/x.so` # where a link belonged -- wrong rather than merely missing. if mode is not None and stat.S_ISLNK(mode): if os.path.lexists(target): os.remove(target) os.symlink(data.decode(), target) continue with open(target, "wb") as fh: fh.write(data) if mode is not None: os.chmod(target, stat.S_IMODE(mode)) if __name__ == "__main__": main(sys.argv)