unpack: open any archive beside itself, never into something that exists¶
The problem¶
Unpacking goes wrong in a handful of familiar ways. A zip with forty loose
files sprays them across Downloads. Always unpacking into a new folder gives
proj-extracted/proj/. A second run writes into the first. The name lies: a
.zip that is a tar, a download with no extension, a .gz with a tar inside.
And an entry named ../../.bashrc writes outside the folder you chose.
The common fix, unpack into a scratch folder in /tmp and move it at the end,
adds two problems of its own: from another disk the "move" is a full copy, and
mkdtemp makes the scratch folder private, so the result comes out mode 700.
The method¶
- Scratch beside the destination. A hidden
.unpack-XXXXfolder in the destination folder sits on the same disk, so placing the result is onerename. Its mode is reset to what the umask gives a new folder. - Claim with
mkdir, then rename onto the claim.mkdir NAME-extractedsucceeds for exactly one caller. POSIXrename()replaces that empty folder with the scratch folder in one step, and fails if anything got into it, so the tool moves on to-2. Eight copies racing on one archive give eight whole, separate folders. - Kind from the first bytes, not the name; after decompressing a stream,
check for a tar (
ustarat byte 257) inside. - Lift a lone folder, except app bundles.
- Keep entries inside. Python's tar
datafilter refuses.., links pointing out and devices; each skip is named and the rest unpacks. A truncated archive's output is kept asNAME-partial, exit 1.
def claim(base, source):
n = 1
while True:
target = base if n == 1 else f"{base}-{n}"
n += 1
try:
os.mkdir(target) # atomic: one winner per name
except FileExistsError:
continue
try:
os.rename(source, target) # replaces the empty claim, or fails
return target
except OSError:
continue # someone wrote into it; next name
unpack release.tar.gz # .../release-extracted
unpack download -o ~/scratch # no extension: read from its bytes
Limits¶
- 7z, rar, iso and friends go to
bsdtar, then 7-Zip, under their own path rules. - No size limit: a zip bomb fills the disk.
- Zip symlinks come out as text files holding the target.
- Disk images and installers need the system's tools; this stays with archives.
- Python 3.12+ (tar
datafilter); zst needs 3.14+ or thezstdcommand.
Code¶
methods/unpack/ in the methods repository: one Python standard-library file and a 32-check test that builds every archive it needs, including unsafe tar entries, a truncated download and eight racing runs.