"""Unpack or repack an OOXML document without shell-specific ZIP commands.""" import argparse import os import tempfile import zipfile from pathlib import Path, PurePosixPath def unpack(source, destination): root = Path(destination).resolve() if root.exists() and any(root.iterdir()): raise ValueError("Unpack destination must be empty") with zipfile.ZipFile(source) as archive: for info in archive.infolist(): name = PurePosixPath(info.filename.replace("\\", "/")) if name.is_absolute() or ".." in name.parts or any(":" in part for part in name.parts): raise ValueError(f"Unsafe ZIP entry: {info.filename}") if (info.external_attr >> 16) & 0o170000 == 0o120000: raise ValueError(f"ZIP symlink is not supported: {info.filename}") archive.extractall(root) def pack(source, destination): root = Path(source).resolve() output = Path(destination).resolve() if output.is_relative_to(root): raise ValueError("Output must be outside the unpacked directory") if not (root / "[Content_Types].xml").is_file(): raise ValueError("Source must be the package root containing [Content_Types].xml") output.parent.mkdir(parents=True, exist_ok=True) fd, temporary = tempfile.mkstemp(suffix=output.suffix, dir=output.parent) os.close(fd) try: with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED) as archive: for file in sorted(root.rglob("*")): if file.is_symlink(): raise ValueError(f"Package symlink is not supported: {file}") if file.is_file(): archive.write(file, file.relative_to(root).as_posix()) os.replace(temporary, output) finally: Path(temporary).unlink(missing_ok=True) if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("action", choices=("unpack", "pack")) parser.add_argument("input") parser.add_argument("output") args = parser.parse_args() {"unpack": unpack, "pack": pack}[args.action](args.input, args.output)