Windows-safe ZIP creation and extraction for macOS, built with Swift/AppKit.
zip-porter replaces the combination of MacWinZipper (creation) and The Unarchiver (extraction, for ZIPs) with a single clean, ad-free, MIT-licensed app that works fully offline:
- Create ZIPs that open correctly on Windows: macOS junk files
(
.DS_Store,__MACOSX/, AppleDouble._*) are excluded, file names are stored as NFC-normalized UTF-8 (no more split-dakuten "テ゛ータ"), and password protection (AES-256) is built in - Extract ZIPs made on Windows: CP932 (Shift_JIS) file names are auto-detected and decoded, and both ZipCrypto and AES encrypted archives are supported
- Legacy compatibility on demand:
--cp932stores file names for old Japanese Windows tools;--zipcryptoproduces archives Windows Explorer can open standalone (with a weakness warning)
See the RFP for the full specification. Output verified on Windows Explorer with real archives (UTF-8 names, junk-free trees, ZipCrypto extraction).
brew install --cask nlink-jp/tap/zip-porter
Or download the notarized .app from
Releases.
Launch ZipPorter and drop things on the window:
- Drop files or folders → an options sheet (password, CP932, ZipCrypto) appears, then a Windows-safe ZIP is created next to the input. The sheet remembers its settings and can be skipped ("Don't show these options again"; re-enable in Settings)
- Drop a
.zip(or double-click one — ZipPorter registers as a ZIP handler) → it is extracted. Encrypted archives prompt for the password - Select several
.zipfiles and open them together → they are extracted as one job: one progress bar across the whole set ("2 of 3 — foo.zip"), one question if the destination is set to ask every time, one password tried across the set, one result, one Finder reveal. An archive that fails does not stop the rest — the result says "2 of 3 extracted" and names what failed - Both operations show a status dialog with a byte-accurate progress bar
while they run. How a clean finish is announced is up to you — a
notification (default), a dialog, or nothing (Settings › General).
Clicking the notification shows the result in Finder. The result dialog
appears regardless when something needs reading — skipped unsafe paths,
skipped symlinks, renamed duplicates, or an archive that failed. Excluded
macOS metadata is routine and just goes in the completion line. A ZIP
being created exists as
name.zip.partuntil it is complete. An archive opened by double-click shows only that dialog — no droplet window — and the app leaves as soon as the work is done; dropping onto the window leaves it open for the next file. Archives opened while one is still running are queued and handled in turn - Settings (the gear in the window, or ⌘,) — The Unarchiver-style extraction preferences: destination (same folder / ask every time / a fixed folder), when to create a wrapper folder (never / only for multiple top-level items / always), the created folder's modification date, reveal-in-Finder, and move-archive-to-Trash. Creating has the same destination choice (next to the originals / a fixed folder / ask every time), plus whether to show the options sheet and whether to reveal the new archive in Finder
- Keyboard — the usual editing shortcuts work in the password fields (⌘X / ⌘C / ⌘V / ⌘A / ⌘Z), and ⌘W closes the droplet window, which ends the session just like clicking its red button
The same binary inside the app doubles as a CLI. Installing the cask puts
zip-porter on your PATH:
brew install --cask nlink-jp/tap/zip-porter
zip-porter --versionIf you installed the .app by hand instead, the executable lives inside
the bundle — link it yourself:
ln -s "/Applications/ZipPorter.app/Contents/MacOS/ZipPorter" /usr/local/bin/zip-porterCommands:
zip-porter pack <input>... [-o <output.zip>] [--password] [--cp932] [--zipcrypto] [--no-clean]
zip-porter unpack <input.zip> [-o <dest-dir>] [--password] [--encoding auto|utf8|cp932]
zip-porter inspect <input.zip>
zip-porter --version
Run with no command to launch the GUI.
- Excludes macOS junk (
.DS_Store,__MACOSX/, AppleDouble._*, FinderIcon\r, Spotlight/fseventsd/Trashes) — disable with--no-clean - File names are NFC-normalized UTF-8 with the UTF-8 flag;
--cp932stores CP932 names instead (names CP932 cannot represent are an error) --passwordprompts interactively (passwords never appear in argv) and encrypts with AES-256 (WinZip AE-2);--zipcryptoselects the weaker Explorer-compatible cipher, with a warning- Already-compressed extensions (jpg, png, mp4, zip, …) are stored, the rest deflated; symlinks are skipped
- An existing output name is never overwritten — "name 2.zip" is used
- Name encoding auto-detection: the UTF-8 flag wins; unflagged names are
validated as UTF-8 first and treated as CP932 otherwise. Override with
--encoding utf8|cp932 - Extracts ZipCrypto and AES-128/192/256 (AE-1/AE-2) archives; prompts for a password on demand
- A single top-level item extracts as itself; anything else is wrapped in a folder named after the archive. Existing files are never overwritten ("name 2")
- Extracts to the archive's own folder by default, or
-o <dir>(created if missing, likeunzip -d)
An unarchiver runs attacker-chosen structure against your filesystem, so extraction refuses malformed archives rather than trying to salvage them (see ADR-0001):
- zip-slip protection — absolute paths,
.., drive letters, and NTFS alternate-data-stream names are skipped and reported; symlink entries are skipped in both directions - Decompression bombs — an entry that expands past the size its header declares is aborted mid-stream, and the whole archive is refused up front when its declared content cannot fit in the destination's free space
- Overlapping entries — archives whose entries share compressed data
(the
42.zipconstruction) are rejected while parsing - Quarantine propagation —
com.apple.quarantineon a downloaded archive is copied onto everything extracted — files and folders alike, including folders the archive only implies — so Gatekeeper still evaluates an app that arrives inside a ZIP. If the attribute cannot be set, the items are named in the result rather than passed over - Permissions — the modes an archive asks for are masked with your
umask, the way
unzipdoes it, so a ZIP cannot drop world-writable files into your folders - Duplicate names — collisions (including case-only and NFC/NFD differences, which APFS treats as one name) are extracted under numbered names instead of silently overwriting each other
- Integrity is verified for every entry: HMAC for AES, CRC-32 otherwise
Entries are compressed in parallel and written in order, so archives remain
byte-for-byte deterministic. Files whose head does not compress are stored
rather than deflated — faster, and smaller. Large files additionally
compress as independent blocks in parallel (zlib, the pigz join), so a
single 180 MB file packs in under a second on a 12-core machine; a
310 MB / 150-file mixed corpus takes 1.8 s where Apple ditto needs 6.9 s
— at the same output size. Design notes:
ADR-0002,
ADR-0003.
Prints each entry's size, method, encryption, and UTF-8-flag state, plus the archive-wide detected name encoding and any macOS junk entries — useful for diagnosing mojibake before extracting.
- macOS 14+ (Apple Silicon)
make build # swift build -c release
make test # swift test
make build-app # assemble + Developer-ID sign dist/ZipPorter.app
make package # notarize + staple + zip the release asset
- AES-256 encrypted ZIPs require 7-Zip (or similar) on Windows — Explorer
alone cannot open them. Use
--zipcryptowhen the recipient can only use Explorer (weaker encryption; a warning is shown).
MIT