Skip to content
zit Artifact [2035876114]
Login

Artifact [2035876114]

Artifact 20358761143cef2734816a1aec13c51bdb5b07cc55350a187c25ca4f120525ac:


#!/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)