Artifact 75f34a3ca04eff4c9ec459825e82043075428a7019e2364536f51ba0f437d85a:
- Executable file
scripts/unzip-preserving-modes.py
— part of check-in
[2420f6fb88]
at
2026-08-17 19:28:02
on branch trunk
— archive differential: unpack zips without an unzip binary (#95)
The runners have no unzip, so the harness exited 2 for a missing tool and the step failed on its own coverage guard instead. Adding a third tool to the battery for one step is the worse trade: python3 is already required here, and ZipFile.extractall drops modes and symlinks -- the exact defect this differential exists to catch -- so the external attributes are applied explicitly rather than trusted to an extractor. (user: dmestas, size: 3054) [annotate] [blame] [check-ins using]
#!/usr/bin/env python3 """Extract a zip, restoring Unix modes and symlinks (#95). `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)