Expand description
The zip family (zip, zipcloak, zipnote, zipsplit): stuffr
behaving as the platform’s Info-ZIP Zip 3.0.
A port of Info-ZIP Zip 3.0 (July 5th 2008; the Info-ZIP licence,
LICENSE-infozip: this is an altered version, marked as such by every
banner) in two builds, Apple’s (zip-33, zip30/, default on macOS and
the BSDs) and Ubuntu’s (3.0-13ubuntu0.2, default elsewhere), chosen by
default_flavour or STUFFR_ZIP_FLAVOR=apple|ubuntu (run reads
it once; anything else in it is refused, exit 1, in stuffr’s own
words). Each ported function carries // port: <file>.c:<function>;
Ubuntu’s differences are checks in flavour.rs, one function per
behaviour. Every string is in messages.rs.
What is here (Task 4 of 0.19.0): the names, the flavour, the
command lines of all four programs (cli.rs: zip.c’s option table and
main()’s loop and checks, fileio.c’s get_option(), util.c‘s
envargs() for ZIPOPT and ZIP, and the three helpers’ own
parsing), and every help, licence, version and option-listing screen.
Task 5 (scan.rs, unix.rs): which names zip operates on: the
operands and -R, -i/-x/-R patterns, -r, -j, -k, -D, -y,
-t/-tt, -@, duplicates and the archive itself, and -sf’s listing
(fileio.c, util.c’s matcher, unix/unix.c); also the unix extra
fields (unix::extra_fields).
Task 6 (zipup.rs, zipfile.rs, write.rs, sink.rs): writing a
new archive, byte for byte as the references write it: zipup.c (the
method, stored or deflated through stuffr::infozip::ZipDeflater,
-l/-ll, the
descriptor or the rewritten local header), zipfile.c’s writing half
(local and central headers, Zip64 fields and end records, the end
record) and zip.c:main from the temporary archive to finish()
(progress counters and dots, unreadable inputs, -c, -z, -m,
-o).
Task 7 (readzip.rs, update.rs): an archive already at the path:
zipfile.c’s reading half (readzipfile, scanzipf_regnew,
readlocal, zipcopy, bfcopy), names matched against its entries
(zsearch, patterns for names not on disk), -u, -f, -d, -FS,
-g, -U, --out, -A, -J, -c/-z over old entries, -T/-TT
(check_zipfile), and zip’s interrupt handler (INT, TERM, ABRT, BUS,
ILL, SEGV: the temporary removed, Interrupted (aborting), exit 9;
host::install_zip_abort; since ruling T7a of the 2026-10-31 security
audit, a signal stuffr sent itself, such as its own abort(), and on
Linux a real memory fault, removes the temporary and is re-raised, so
stuffr’s own crash is not reported as an interrupt: deviation 34).
Task 8 (split.rs, fix.rs, readzip.rs): split archives read
(every part through Host::track, zip’s questions when a part cannot be
opened) and written (-s, -sp, -sb, -sv: bfwrite’s other half,
the local header’s split kept open for its rewrite, a split archive
read and copied split as its first part is); -F (scanzipf_regnew’s
fix mode: the offset of bytes before the archive, damaged central
records skipped, a bad entry written over) and -FF (scanzipf_fixnew:
the archive read from its first byte, every local header copied as it
is met, a central header found later filling in what only it has).
Task 9 (ttyio.rs, zipcloak.rs, zipup.rs): encryption (-P,
-e): crypthead‘s header after each local header (its check the
time’s, the entry always with a data descriptor), the data encrypted as
zfwrite encrypts it; the password prompts on the controlling terminal
(getp, unzip’s port, the terminal put back by the interrupt handler
too); and zipcloak (encrypt, -d, -O, -b), whose messages,
warnings and errors are the helpers’ (zipcloak warning:, zipcloak error:, perror("zipcloak error")).
Task 10 (zipnote.rs, zipsplit.rs): zipnote (the comments listed,
-w rewriting the archive with new comments and names) and zipsplit
(-n, -r, -t, -i, -p, -s, -b: the sequential and the greedy
split, the parts and the index written directly under their names), over
the shared reader and writer.
Task 11 (unicode.rs, zipup.rs, flavour.rs): Ubuntu’s deltas:
UNICODE_SUPPORT (bit 11 and the Unicode Path field written and read,
-UN, -su/-sU, matching by the Unicode name, the helpers and -FF
over such entries), BZIP2_SUPPORT (-Z bzip2 through stuffr’s bzip2
encoder at zip’s block size), and the thirteen-patch series (listed in
flavour.rs with each hook or why it has none).
Zr is G: zip’s globals are one value per run (Zr, with its
options o), so a sandboxed run ([run_in_counted]) touches nothing of
the process.
Known deviations (the source of truth; the README and the spec mirror this list):
- SIGPIPE:
runrestores SIGPIPE’s default action, as the other families do; under a parent that ignores SIGPIPE, zip seesEPIPEand stuffr still dies of the signal (the Rust runtime ignores it beforemain). - The banner line of
-vand the header line of each program’s help name stuffr and say it is altered (Info-ZIP licence clause 3):This is Zip 3.0 (July 5th 2008), by stuffr X.Y.Z (altered; original by Info-ZIP, with modifications by Apple Inc.).(Ubuntu:(altered; Debian's build, original by Info-ZIP)), likewiseZip 3.0 (July 5th 2008), by stuffr ... Usage:and the helpers’ZipCloak 3.0 (July 5th 2008), by stuffr ....-v’sCompiled withline is the recorded reference build’s (Apple: macOS 27’s), so another macOS build prints a different one. - Undefined behaviour and endless loops are not reproduced (the
record’s rulings, in the code’s
// ruling:comments); 5 and 6 are the ones the command line meets. - Encryption is compatible, not identical: the ten random bytes of each
12-byte encryption header come from the system’s generator
(
getentropy,getrandomor/dev/urandom; a system that gives none ends the run,Internal logic error, exit 5), where zip’s come fromrand()seeded with the time, so an encrypted entry’s header and data differ from the reference’s run to run (every other byte of the archive is the same, and it decrypts the same). get_option’s error text is built in a 50-byte buffer (optionerr’schar optname[50],sprintf). An error about an option whose'name' (description)needs 50 bytes or more (-lf,--output-file,--junk-paths=x,--recurse-patterns-: 59 forms in all, listed bycli/tests.rs) aborts both references (Apple: SIGTRAP from__sprintf_chk, Ubuntu: glibc’s “buffer overflow detected” and zip’s SIGABRT handler, exit 9). stuffr prints the whole message, exit 16, as the C would with room.-O(--output-file) with no archive name at all: the C compares a NULLin_path(namecmp(in_path, out_path)) and dies of SIGSEGV, which zip’s handler answers in both builds with a blank line andzip error: Interrupted (aborting), exit 9. stuffr gives that answer without the dereference.- The “Scanning files” progress message
newnameprints when a scan of 100 or more names takes over five seconds, with a dot every two seconds after, is not made (it depends on the clock, not on the names). - A name that is
//hostor//host/shareand nothing more makesex2indereference NULL in both references: SIGSEGV, which zip’s handler answers with a blank line andInterrupted (aborting), exit 9. stuffr gives that answer without the dereference (as for 6), also for an-i/-x/-Rpattern of that shape. The order of two equal names in the found list follows each libc’sqsort(Ubuntu’s stable merge sort; Apple’s, on macOS, the platform’s ownqsort_r, and off macOS a BSD quicksort ported inscan.rs). Off macOS the port orders 6 of 4000 random arrays with many equal keys differently from macOS (a known gap: which of two equal names is dropped, when the same operand is given twice among many). -cor-zafter-@(or with zip as a filter) read standard error (fdopen(fileno(stderr), "r")). When it is not open for reading (a pipe, a file opened to write)fdopenfails and the C’sfgets(NULL)dies of SIGSEGV in both references, which zip’s handler answers with a blank line andInterrupted (aborting), exit 9, the temporary removed. stuffr gives that answer without the dereference (as 6 and 8), at the first question, and a sandbox’s standard error is never readable.- A
-cor-zline is cut at its first NUL, as the C’sstrlenandstrcpycut it; a line that BEGINS with a NUL makes the C read the byte before its buffer (p[k-1],e[r-1]with a length of 0). stuffr takes it for an empty line. - A symbolic link stored under
-ythat is removed between zip’sstatand itsreadlinkmakes the C passreadlink’s -1 on as a length tocrc32andzfwrite. stuffr stores the link with no text (0 bytes, CRC 0), exit 0. - A write to the archive that fails is answered where and as zip’s
stdio stream answers it (
zipup.rs,sink.rs; measured underulimit -fon both references). Two Ubuntu answers are not reproduced: when onlyfclosemeets the failure, Ubuntu’sziperrcloses the stream a second time and glibc aborts on the double free (exit 5, the temporary left; undefined behaviour, as ruling Z3’s); stuffr answers as Apple does (“Temporary file failure”, exit 10, the temporary removed). And thezip I/O error:line of “unexpected error on zip file” shows whatevererrnoglibc’s later calls left (No such file or directoryfrom the time zone lookup in a container without tzdata); stuffr shows the write’s own error, as Apple does. - Reading an archive, undefined behaviour of the C is not reproduced:
an end record or Zip64 record cut short is read as zeros (the C reads
its uninitialised stack buffer: Apple, for an archive that is only
PK\5\6, answers “cannot update a split archive”; stuffr sees an empty archive); a Zip64 extra field shorter than the sizes it should hold is read as zeros past its end;-X-stops copying old extra blocks at one that runs past its field. And-g(grow) writes a copy of the archive up to its central directory, appends, adds whatever the old file had past the new end (zip writes in place and never truncates, so a shorter-zcomment leaves the old tail), and renames the copy into place: the bytes of a successful run are zip’s. Except under--out(ruling Z33): zip opens the--outfile withFOPW(truncating it to nothing), grows the input archive in place and skipsreplace, sozip -g in.zip --out out.zip fleavesout.zipempty andin.zipgrown; stuffr leavesin.zipuntouched and publishes the grown copy asout.zip(pinned byupdate::tests::growing_with_out_publishes_the_copy). A failure leaves the archive as it was: zip printsattempting to restore <zip> to its previous state(so does stuffr, on every failure and interrupt under-g) and rewrites the old central directory in place, leaving what it appended after it (a write failure there can even leave no readable archive). A failed-T -gis undefined behaviour in the C (fseekoon the closed stream: Apple exits 5 after a blank line); stuffr answers with the restore line, exit 8.-T -gtests the copy, so a-TTcommand is given the temporary’s name, not the archive’s. - zip opens the output to check that it can write it with
fopen "wb"(truncating it) when it is--out’s or the archive has no entries, and then removes an archive without entries; stuffr opens it without truncating and removes nothing it did not create. So a run that fails or is interrupted leaves an--outtarget or an entry-less archive as it was (zip leaves the first empty and the second gone). And on success, an entry-less archive that is a symbolic or hard link: zip, having removed the link, renames a new file into its place (the link’s target left empty); stuffr writes through the link, asreplacedoes for any linked archive. Pinned byupdate::tests::a_failed_run_leaves_the_output_as_it_was. - Ruling Z3: a failing
-Tin Ubuntu’s build closes the temporary’s stream twice (ziperrafterfclose(y)) and glibc aborts on the double free (exit 5, a blank line, glibc’s message); stuffr answers as Apple’s build does in both flavours:test of t.zip FAILED,zip error: Zip file invalid, could not spawn unzip, or wrong unzip (original files unmodified), exit 8, the temporary removed. - A sandboxed run ([
run_in]) spawns nounzipand no-TTcommand:-Tfails there as without anunziponPATH. The interrupt handler writesInterrupted (aborting)tomesgonly; a-lflog does not get it. - Ruling Z16:
-sf -m(--show-fileswith move) lists and ends without writing an archive, and zip’sfinish()then deletes the files of the entries the names marked, though nothing holds them. stuffr deletes nothing after-sf. - Ruling Z5: zip reads the answers to its questions about split parts
with
fgetsand never looks at what it returns. At the end of standard input its buffer keeps what it held (uninitialised at the first question), and-FF(both builds; also-Fand a copy of a split archive whose part is missing) asks again for ever, gigabytes of prompts; so does Ubuntu’s-sp. stuffr never loops: the end of standard input at a question about a part to READ answersq(abort: as an explicitq, measured on both references:-FFgoes on and writes what it has,could not find splitand exit 0 when the part was the one to scan,Interrupted (Could not find split), exit 9, when an entry was being copied), and at-sp’s question about where to WRITE the next split it answers ENTER (that question has no way out but an interrupt; ENTER is what Apple’s build does). -FF, undefined behaviour of the C, not reproduced: the central extra field of an entry-FFfound by its local header is the local one copied withstrcpy, which stops at the first NUL (almost always the second byte of the first block’s size) and leaves the rest of themalloced field as it was: zeros on Apple, heap garbage that changes from run to run on Ubuntu. stuffr copies up to and with the first NUL and writes zeros after it (Apple’s bytes). The entry’slflg, never set, is read as 0 (it only sizestempzn); a data descriptor signature split across two 16 KiB reads (the C then writeskk - 3, wrapped, bytes) is not taken for one; a Zip64 extra field shorter than its sizes is read as zeros past its end.- Ruling Z21:
-F/-FFwhose--outis the damaged archive under another name (./a.zip, a hard or symbolic link to it) is refused as the same name is (--out path must be different than in path, exit 16), before anything is written: zip compares the names only and renames its lossy salvage over the archive it salvaged from. - Ruling Z23: when what did not fit in a split cannot be written to
the next one (“Not enough room on disk”),
-sp(split_method2) lets the user choose another disk and opens another split, for ever on a full disk; stuffr warns as-spdoes and ends with-s’s answer,zip error: Output file write failure (Not enough room on disk), exit 14. - Ruling Z24:
-spwith an archive name that has no directory: zip takes the name part as empty, so an answer (a directory, a bare name) makes the next split’s name from a name shorter thanzipand indexes before its buffer. stuffr takes such a name as one in the current directory:zip -s 64k -sp t.zip bigansweredo/writest.z01here, theno/t.z02, …,o/t.zip. - Ruling Z25:
-zwhose first comment line is empty copies\r\ninto a one-byte block (strcpy(p, "\r\n")): Ubuntu’s build dies in glibc’s buffer-overflow check (exit 9 through zip’s SIGABRT handler); Apple’s writes the comment. stuffr writes it in both flavours, as Apple’s build does. zipcloak(crypt.c’szipcloak()andzipbare()) ignoresreadlocal()’s failure (a local header that cannot be found or read) and goes on with an uninitialised local header; stuffr ends the run there with that failure’s code (zipcloak error: ... (was encrypting an entry)or(was decrypting an entry)), the temporary removed and the archive untouched. A split archive under-Omeets it at its first entry: zipcloak’sreadlocal()(the UTIL build’s) looks for every entry in the last part only, warnsDid not find entry for <name>, and Ubuntu’s build then dies of SIGSEGV (Interrupted (aborting), exit 9, or killed by the signal from its handler; measured with a terminal in Docker); stuffr warns the same and ends withZip file structure invalid, exit 3. (Without-Oboth refuse a split archive:cannot update a split archive, exit 16.) A central name longer than the local one makesreadlocal()write past the local name’s buffer; stuffr keeps the local name as it is (a shorter central name cuts it with a NUL, as the C does).zipnoteandzipsplitnever setout_path, soreadzipfile()‘sstrcmp(in_path, out_path)over a NULL ends in SIGSEGV for an archive that does not exist (or is named ``) and for a split archive (a.zipthat says it is one of several): both references’ handler printsInterrupted (aborting)and exits 9. stuffr answers the same (readzip::null_out_crash); the signal is not raised.- Ruling Z4: Apple’s
zipnote -wcloses the input stream twice (the secondfclosecloses the temporary’s) and always fails, exit 10,Bad file descriptorthenTemporary file failure, with the archive untouched. That is undefined behaviour; stuffr takes Ubuntu’s patched answer (patch 07) in both flavours:-wworks. Pinned bycompat_zip_helpers::zipnote_w_is_the_same_in_both_flavours(stuffr’s two flavours agree and exit 0) and, on Ubuntu, against the reference. A comment’s length wraps at 65536 as the C’sushdoes. zipsplit: a%in the archive’s name is a conversion in the C’s name template (sprintf(name, template, n), undefined behaviour); stuffr takes it as a character. After an error writing the index under-pand a yes to “redo entire disk?”, the C goes back to the wrong disk (its loop variable holds the failure flag); stuffr redoes disk 1. An interrupt removes the parts only when-b’s directory is shorter than half ofPATH_MAX(the handler’s static holds that much; longer arms nothing and the parts stay).- Ubuntu’s
UNICODE_SUPPORT(unicode.rs), undefined behaviour of the C not reproduced. Where a conversion fails and the C reads through the NULL it got (a bit-11 name zip’s UTF-8 decoder refuses; a Unicode Path field whose UTF-8 name it refuses;-UN escapeover a found name the locale cannot convert) or copies the entry’s not-yet-set name (a Unicode Path field with no name, read from the central directory or byreadlocal), the reference dies of SIGSEGV and its handler answersInterrupted (aborting), exit 9; stuffr gives that answer without the crash (unicode::segv), as rulings Z10/Z13 do. A Unicode Path field shorter than its header says is read as zeros past its end (the C reads past it); an old field of another length than the new one is taken out and the new one appended (the C’s copy of what follows it runs four bytes long). Patch 12 (mbstowcstold the buffer holdsstrlen + 1wide characters, which glibc’s fortify takes for an overflow and aborts on, for every name that is not ASCII) and patch 13 (-TT’s command buffer one byte short) are Ubuntu’s own fixes: both flavours take the patched, defined behaviour (Apple has neither code path). Pinned, stuffr-only, bycompat_zip_common::ub_rulings; the crash answers are also compared with the reference (compat_zip_ubuntu). -Z bzip2(Ubuntu): zipup.c tests whether an entry is text over the whole 16 KiB input buffer, past the bytes read, and the buffer is allocated once per run and never cleared, so the test sees what an earlier entry left there (reproduced: a short text file after a longer text file is text). The block’s first contents are uninitialised in the C; stuffr takes them as zeros (a short first text file is binary,iattr0, as measured). Pinned bycompat_zip_common::ub_rulings.- Ruling Z32: an interrupt (SIGINT, SIGTERM) while the new archive
replaces the old one (
fileio.c:replace: the archive unlinked, then the temporary renamed; or, for a linked archive or a temporary on another device such as-b’s, the archive truncated and the temporary copied into it) makes zip’s handler remove the temporary, which leaves the archive gone or cut short and no whole copy anywhere. stuffr holds both signals back for the length of the replace (zip, its split parts and-F/-FF,zipcloak,zipnote -w;-g’s restore state is cleared in the same window) and answers them as zip does once the archive is whole:Interrupted (aborting), exit 9, the new archive in place. A long cross-device copy answers Ctrl-C when it ends. When the replace fails after the archive was unlinked or cut (ruling Z34), the temporary is the only whole copy: it is kept under its own name, where zip’s handler would remove it. Pinned bytests::an_interrupt_during_replace_leaves_a_whole_archiveandtests::an_interrupt_after_a_failed_replace_keeps_the_temporary. - Hardening (ruling T5 of the 2026-10-31 security audit): zip never
writes through a symbolic link at an output name that is not the
archive it read. Info-ZIP follows any link there (
fopen, thenfileio.c:replace’s copy for a symlinked archive): a dangling one creates the file it names, a live one at-O/--outtruncates its target. stuffr refuses, before any work, a link at-O/--out(also-F/-FF’s output andzipcloak -O) whatever it points at, and a link at the archive’s own name unless it leads to a non-empty regular file (an archive zip read): dangling, or naming an empty file, a directory or a device. The refusal iszip I/O error: Too many levels of symbolic links,zip error: Could not create output file (NAME), exit 15, the link kept, no temporary left. An archive reached through a link to a non-empty file is still updated through it, as Info-ZIP updates it (replace’s copy), andreplacechecks again (split parts as-Onames);zipnote -wshares it. Why: a planted link must not redirect where an archive is written. - Hardening (ruling T5):
zipsplitopens its index and each partO_NOFOLLOW. zipsplit truncates whatever is at the name (fopen(.., "w")), writing through a symlink there, live or dangling; stuffr fails the open (zipsplit error: Too many levels of symbolic links,Could not create output file (NAME), exit 15) and its cleanup leaves the link, which it did not make. A regular file at a part’s name is still truncated, as zipsplit truncates it. - Hardening (ruling T6 of the 2026-10-31 security audit): the
-lflog’s command line shows-P’s password as********(eight asterisks, whatever its length), in every form zip accepts (-P pw,-Ppw,-P=pw,-qPpw,--password pw,--password=pwand its abbreviations, fromZIPOPT/ZIPtoo); zip writes it as given. A new log is created 0600 (zip:0666 & ~umask), an existing one keeps its mode, and the log,-la’s included, is openedO_NOFOLLOW: a symlink at its name, live or dangling, iscould not open logfile, exit 16, where zip writes through it.-sc’s listing still shows the password, as zip’s does (it goes to the terminal, not to a file). zip and zipcloak keep the password, given or typed, only in a holder that zeroes it when dropped (ruling T6b), and the traditional-encryption keys derived from it (infozip::crypt::Keys) are zeroed when dropped too. Only those are: the process’s own arguments (argv), and the parser’s copies of them (z.o.args, which-scand-R’s hint print), still hold the-Pvalue and are not zeroed. - Hardening (ruling T7a of the 2026-10-31 security audit): zip’s
handler answers every INT, TERM, ABRT, BUS, ILL and SEGV with
Interrupted (aborting), exit 9. stuffr’s answers a signal it sent itself (its ownabort(): a double panic, an allocation failure, a corrupted heap) and, on Linux, a SEGV, BUS or ILL the kernel raised for a real fault by removing the temporary (and zipsplit’s parts) and dying of the signal at its default action, without the line: a crash in stuffr stays a crash. A signal another process sends (kill -SEGVtoo) keeps zip’s answer. On macOS, which reports akill -SEGV/-BUS/-ILLexactly as a fault, a real fault still answers as zip does. A stack overflow dies of the signal with no message (the handler has no stack to run on).
Structs§
- Exit
- How a run ends: the exit code (
ziperr()’s,EXIT()’s, or a screen asked for with exit 0).
Enums§
- Flavour
- Which reference the family behaves as.
Functions§
- default_
flavour - The flavour a build uses when
STUFFR_ZIP_FLAVORis unset or empty: Apple on macOS and the BSDs, Ubuntu everywhere else, Windows included. - run
- Entry point for
zip,zipcloak,zipnoteandzipsplit;nameis the canonical invoked name,argv0the program as invoked (-scshows it) andargsthe arguments after it.