Skip to main content

stuffr_cli/compat/zip/
mod.rs

1//! The zip family (`zip`, `zipcloak`, `zipnote`, `zipsplit`): stuffr
2//! behaving as the platform's Info-ZIP Zip 3.0.
3//!
4//! **A port** of Info-ZIP Zip 3.0 (July 5th 2008; the Info-ZIP licence,
5//! `LICENSE-infozip`: this is an altered version, marked as such by every
6//! banner) in two builds, Apple's (`zip-33`, `zip30/`, default on macOS and
7//! the BSDs) and Ubuntu's (`3.0-13ubuntu0.2`, default elsewhere), chosen by
8//! [`default_flavour`] or `STUFFR_ZIP_FLAVOR=apple|ubuntu` ([`run`] reads
9//! it once; anything else in it is refused, exit 1, in stuffr's own
10//! words). Each ported function carries `// port: <file>.c:<function>`;
11//! Ubuntu's differences are checks in `flavour.rs`, one function per
12//! behaviour. Every string is in `messages.rs`.
13//!
14//! **What is here (Task 4 of 0.19.0):** the names, the flavour, the
15//! command lines of all four programs (`cli.rs`: `zip.c`'s option table and
16//! `main()`'s loop and checks, `fileio.c`'s `get_option()`, `util.c`'s
17//! `envargs()` for `ZIPOPT` and `ZIP`, and the three helpers' own
18//! parsing), and every help, licence, version and option-listing screen.
19//!
20//! **Task 5 (`scan.rs`, `unix.rs`):** which names `zip` operates on: the
21//! operands and `-R`, `-i`/`-x`/`-R` patterns, `-r`, `-j`, `-k`, `-D`, `-y`,
22//! `-t`/`-tt`, `-@`, duplicates and the archive itself, and `-sf`'s listing
23//! (`fileio.c`, `util.c`'s matcher, `unix/unix.c`); also the unix extra
24//! fields (`unix::extra_fields`).
25//!
26//! **Task 6 (`zipup.rs`, `zipfile.rs`, `write.rs`, `sink.rs`):** writing a
27//! new archive, byte for byte as the references write it: `zipup.c` (the
28//! method, stored or deflated through `stuffr::infozip::ZipDeflater`,
29//! `-l`/`-ll`, the
30//! descriptor or the rewritten local header), `zipfile.c`'s writing half
31//! (local and central headers, Zip64 fields and end records, the end
32//! record) and `zip.c:main` from the temporary archive to `finish()`
33//! (progress counters and dots, unreadable inputs, `-c`, `-z`, `-m`,
34//! `-o`).
35//!
36//! **Task 7 (`readzip.rs`, `update.rs`):** an archive already at the path:
37//! `zipfile.c`'s reading half (`readzipfile`, `scanzipf_regnew`,
38//! `readlocal`, `zipcopy`, `bfcopy`), names matched against its entries
39//! (`zsearch`, patterns for names not on disk), `-u`, `-f`, `-d`, `-FS`,
40//! `-g`, `-U`, `--out`, `-A`, `-J`, `-c`/`-z` over old entries, `-T`/`-TT`
41//! (`check_zipfile`), and zip's interrupt handler (INT, TERM, ABRT, BUS,
42//! ILL, SEGV: the temporary removed, `Interrupted (aborting)`, exit 9;
43//! `host::install_zip_abort`).
44//!
45//! **Task 8 (`split.rs`, `fix.rs`, `readzip.rs`):** split archives read
46//! (every part through `Host::track`, zip's questions when a part cannot be
47//! opened) and written (`-s`, `-sp`, `-sb`, `-sv`: `bfwrite`'s other half,
48//! the local header's split kept open for its rewrite, a split archive
49//! read and copied split as its first part is); `-F` (`scanzipf_regnew`'s
50//! fix mode: the offset of bytes before the archive, damaged central
51//! records skipped, a bad entry written over) and `-FF` (`scanzipf_fixnew`:
52//! the archive read from its first byte, every local header copied as it
53//! is met, a central header found later filling in what only it has).
54//!
55//! **Task 9 (`ttyio.rs`, `zipcloak.rs`, `zipup.rs`):** encryption (`-P`,
56//! `-e`): `crypthead`'s header after each local header (its check the
57//! time's, the entry always with a data descriptor), the data encrypted as
58//! `zfwrite` encrypts it; the password prompts on the controlling terminal
59//! (`getp`, unzip's port, the terminal put back by the interrupt handler
60//! too); and `zipcloak` (encrypt, `-d`, `-O`, `-b`), whose messages,
61//! warnings and errors are the helpers' (`zipcloak warning:`, `zipcloak
62//! error:`, `perror("zipcloak error")`).
63//!
64//! **Task 10 (`zipnote.rs`, `zipsplit.rs`):** `zipnote` (the comments listed,
65//! `-w` rewriting the archive with new comments and names) and `zipsplit`
66//! (`-n`, `-r`, `-t`, `-i`, `-p`, `-s`, `-b`: the sequential and the greedy
67//! split, the parts and the index written directly under their names), over
68//! the shared reader and writer.
69//!
70//! **Task 11 (`unicode.rs`, `zipup.rs`, `flavour.rs`):** Ubuntu's deltas:
71//! `UNICODE_SUPPORT` (bit 11 and the Unicode Path field written and read,
72//! `-UN`, `-su`/`-sU`, matching by the Unicode name, the helpers and `-FF`
73//! over such entries), `BZIP2_SUPPORT` (`-Z bzip2` through stuffr's bzip2
74//! encoder at zip's block size), and the thirteen-patch series (listed in
75//! `flavour.rs` with each hook or why it has none).
76//!
77//! **Zr is `G`:** zip's globals are one value per run (`Zr`, with its
78//! options `o`), so a sandboxed run ([`run_in_counted`]) touches nothing of
79//! the process.
80//!
81//! Known deviations (the source of truth; the README and the spec mirror
82//! this list):
83//!
84//! 1. SIGPIPE: [`run`] restores SIGPIPE's default action, as the other
85//!    families do; under a parent that ignores SIGPIPE, zip sees `EPIPE`
86//!    and stuffr still dies of the signal (the Rust runtime ignores it
87//!    before `main`).
88//! 2. The banner line of `-v` and the header line of each program's help
89//!    name stuffr and say it is altered (Info-ZIP licence clause 3):
90//!    `This is Zip 3.0 (July 5th 2008), by stuffr X.Y.Z (altered; original
91//!    by Info-ZIP, with modifications by Apple Inc.).` (Ubuntu: `(altered;
92//!    Debian's build, original by Info-ZIP)`), likewise `Zip 3.0 (July 5th
93//!    2008), by stuffr ... Usage:` and the helpers' `ZipCloak 3.0 (July
94//!    5th 2008), by stuffr ...`. `-v`'s `Compiled with` line is the
95//!    recorded reference build's (Apple: macOS 27's), so another macOS
96//!    build prints a different one.
97//! 3. Undefined behaviour and endless loops are not reproduced (the
98//!    record's rulings, in the code's `// ruling:` comments); 5 and 6 are
99//!    the ones the command line meets.
100//! 4. Encryption is compatible, not identical: the ten random bytes of each
101//!    12-byte encryption header come from the system's generator
102//!    (`getentropy`, `getrandom` or `/dev/urandom`; a system that gives
103//!    none ends the run, `Internal logic error`, exit 5), where zip's come
104//!    from `rand()` seeded with the time,
105//!    so an encrypted entry's header and data differ from the reference's
106//!    run to run (every other byte of the archive is the same, and it
107//!    decrypts the same).
108//! 5. `get_option`'s error text is built in a 50-byte buffer
109//!    (`optionerr`'s `char optname[50]`, `sprintf`). An error about an
110//!    option whose `'name' (description)` needs 50 bytes or more (`-lf`,
111//!    `--output-file`, `--junk-paths=x`, `--recurse-patterns-`: 59 forms in
112//!    all, listed by `cli/tests.rs`) aborts both references (Apple: SIGTRAP
113//!    from `__sprintf_chk`, Ubuntu: glibc's "buffer overflow detected" and
114//!    zip's SIGABRT handler, exit 9). stuffr prints the whole message,
115//!    exit 16, as the C would with room.
116//! 6. `-O` (`--output-file`) with no archive name at all: the C compares a
117//!    NULL `in_path` (`namecmp(in_path, out_path)`) and dies of SIGSEGV,
118//!    which zip's handler answers in both builds with a blank line and
119//!    `zip error: Interrupted (aborting)`, exit 9. stuffr gives that answer
120//!    without the dereference.
121//! 7. The "Scanning files" progress message `newname` prints when a scan
122//!    of 100 or more names takes over five seconds, with a dot every two
123//!    seconds after, is not made (it depends on the clock, not on the
124//!    names).
125//! 8. A name that is `//host` or `//host/share` and nothing more makes
126//!    `ex2in` dereference NULL in both references: SIGSEGV, which zip's
127//!    handler answers with a blank line and `Interrupted (aborting)`, exit
128//!    9. stuffr gives that answer without the dereference (as for 6), also
129//!    for an `-i`/`-x`/`-R` pattern of that shape. The order of two equal
130//!    names in the found list follows each libc's `qsort` (Ubuntu's stable
131//!    merge sort; Apple's, on macOS, the platform's own `qsort_r`, and off
132//!    macOS a BSD quicksort ported in `scan.rs`). Off macOS the port orders
133//!    6 of 4000 random arrays with many equal keys differently from macOS
134//!    (a known gap: which of two equal names is dropped, when the same
135//!    operand is given twice among many).
136//! 9. `-c` or `-z` after `-@` (or with zip as a filter) read standard error
137//!    (`fdopen(fileno(stderr), "r")`). When it is not open for reading (a
138//!    pipe, a file opened to write) `fdopen` fails and the C's `fgets(NULL)`
139//!    dies of SIGSEGV in both references, which zip's handler answers with
140//!    a blank line and `Interrupted (aborting)`, exit 9, the temporary
141//!    removed. stuffr gives that answer without the dereference (as 6 and
142//!    8), at the first question, and a sandbox's standard error is never
143//!    readable.
144//! 10. A `-c` or `-z` line is cut at its first NUL, as the C's `strlen` and
145//!     `strcpy` cut it; a line that BEGINS with a NUL makes the C read the
146//!     byte before its buffer (`p[k-1]`, `e[r-1]` with a length of 0).
147//!     stuffr takes it for an empty line.
148//! 11. A symbolic link stored under `-y` that is removed between zip's
149//!     `stat` and its `readlink` makes the C pass `readlink`'s -1 on as a
150//!     length to `crc32` and `zfwrite`. stuffr stores the link with no
151//!     text (0 bytes, CRC 0), exit 0.
152//! 12. A write to the archive that fails is answered where and as zip's
153//!     stdio stream answers it (`zipup.rs`, `sink.rs`; measured under
154//!     `ulimit -f` on both references). Two Ubuntu answers are not
155//!     reproduced: when only `fclose` meets the failure, Ubuntu's `ziperr`
156//!     closes the stream a second time and glibc aborts on the double free
157//!     (exit 5, the temporary left; undefined behaviour, as ruling Z3's);
158//!     stuffr answers as Apple does ("Temporary file failure", exit 10, the
159//!     temporary removed). And the `zip I/O error:` line of "unexpected
160//!     error on zip file" shows whatever `errno` glibc's later calls left
161//!     (`No such file or directory` from the time zone lookup in a
162//!     container without tzdata); stuffr shows the write's own error, as
163//!     Apple does.
164//! 13. Reading an archive, undefined behaviour of the C is not reproduced:
165//!     an end record or Zip64 record cut short is read as zeros (the C reads
166//!     its uninitialised stack buffer: Apple, for an archive that is only
167//!     `PK\5\6`, answers "cannot update a split archive"; stuffr sees an
168//!     empty archive); a Zip64 extra field shorter than the sizes it should
169//!     hold is read as zeros past its end; `-X-` stops copying old extra
170//!     blocks at one that runs past its field. And `-g` (grow) writes a copy
171//!     of the archive up to its central directory, appends, adds whatever
172//!     the old file had past the new end (zip writes in place and never
173//!     truncates, so a shorter `-z` comment leaves the old tail), and
174//!     renames the copy into place: the bytes of a successful run are
175//!     zip's. Except under `--out` (ruling Z33): zip opens the `--out` file
176//!     with `FOPW` (truncating it to nothing), grows the input archive in
177//!     place and skips `replace`, so `zip -g in.zip --out out.zip f` leaves
178//!     `out.zip` empty and `in.zip` grown; stuffr leaves `in.zip` untouched
179//!     and publishes the grown copy as `out.zip` (pinned by
180//!     `update::tests::growing_with_out_publishes_the_copy`). A failure
181//!     leaves the archive as it was: zip prints
182//!     `attempting to restore <zip> to its previous state` (so does
183//!     stuffr, on every failure and interrupt under `-g`) and rewrites the
184//!     old central directory in place, leaving what it appended after it
185//!     (a write failure there can even leave no readable archive). A
186//!     failed `-T -g` is undefined behaviour in the C (`fseeko` on the
187//!     closed stream: Apple exits 5 after a blank line); stuffr answers
188//!     with the restore line, exit 8. `-T -g` tests the copy, so a `-TT`
189//!     command is given the temporary's name, not the archive's.
190//! 14. zip opens the output to check that it can write it with `fopen
191//!     "wb"` (truncating it) when it is `--out`'s or the archive has no
192//!     entries, and then removes an archive without entries; stuffr opens
193//!     it without truncating and removes nothing it did not create. So a
194//!     run that fails or is interrupted leaves an `--out` target or an
195//!     entry-less archive as it was (zip leaves the first empty and the
196//!     second gone). And on success, an entry-less archive that is a
197//!     symbolic or hard link: zip, having removed the link, renames a new
198//!     file into its place (the link's target left empty); stuffr writes
199//!     through the link, as `replace` does for any linked archive. Pinned by
200//!     `update::tests::a_failed_run_leaves_the_output_as_it_was`.
201//! 15. Ruling Z3: a failing `-T` in Ubuntu's build closes the temporary's
202//!     stream twice (`ziperr` after `fclose(y)`) and glibc aborts on the
203//!     double free (exit 5, a blank line, glibc's message); stuffr answers
204//!     as Apple's build does in both flavours: `test of t.zip FAILED`, `zip
205//!     error: Zip file invalid, could not spawn unzip, or wrong unzip
206//!     (original files unmodified)`, exit 8, the temporary removed.
207//! 16. A sandboxed run ([`run_in`]) spawns no `unzip` and no `-TT`
208//!     command: `-T` fails there as without an `unzip` on `PATH`. The
209//!     interrupt handler writes `Interrupted (aborting)` to `mesg` only; a
210//!     `-lf` log does not get it.
211//! 17. Ruling Z16: `-sf -m` (`--show-files` with move) lists and ends
212//!     without writing an archive, and zip's `finish()` then deletes the
213//!     files of the entries the names marked, though nothing holds them.
214//!     stuffr deletes nothing after `-sf`.
215//! 18. Ruling Z5: zip reads the answers to its questions about split parts
216//!     with `fgets` and never looks at what it returns. At the end of
217//!     standard input its buffer keeps what it held (uninitialised at the
218//!     first question), and `-FF` (both builds; also `-F` and a copy of a
219//!     split archive whose part is missing) asks again for ever, gigabytes
220//!     of prompts; so does Ubuntu's `-sp`. stuffr never loops: the end of
221//!     standard input at a question about a part to READ answers `q`
222//!     (abort: as an explicit `q`, measured on both references: `-FF` goes
223//!     on and writes what it has, `could not find split` and exit 0 when
224//!     the part was the one to scan, `Interrupted (Could not find split)`,
225//!     exit 9, when an entry was being copied), and at `-sp`'s question
226//!     about where to WRITE the next split it answers ENTER (that question
227//!     has no way out but an interrupt; ENTER is what Apple's build does).
228//! 19. `-FF`, undefined behaviour of the C, not reproduced: the central
229//!     extra field of an entry `-FF` found by its local header is the local
230//!     one copied with `strcpy`, which stops at the first NUL (almost always
231//!     the second byte of the first block's size) and leaves the rest of
232//!     the `malloc`ed field as it was: zeros on Apple, heap garbage that
233//!     changes from run to run on Ubuntu. stuffr copies up to and with the
234//!     first NUL and writes zeros after it (Apple's bytes). The entry's
235//!     `lflg`, never set, is read as 0 (it only sizes `tempzn`); a data
236//!     descriptor signature split across two 16 KiB reads (the C then
237//!     writes `kk - 3`, wrapped, bytes) is not taken for one; a Zip64 extra
238//!     field shorter than its sizes is read as zeros past its end.
239//! 20. Ruling Z21: `-F`/`-FF` whose `--out` is the damaged archive under
240//!     another name (`./a.zip`, a hard or symbolic link to it) is refused
241//!     as the same name is (`--out path must be different than in path`,
242//!     exit 16), before anything is written: zip compares the names only
243//!     and renames its lossy salvage over the archive it salvaged from.
244//! 21. Ruling Z23: when what did not fit in a split cannot be written to
245//!     the next one ("Not enough room on disk"), `-sp` (`split_method` 2)
246//!     lets the user choose another disk and opens another split, for ever
247//!     on a full disk; stuffr warns as `-sp` does and ends with `-s`'s
248//!     answer, `zip error: Output file write failure (Not enough room on
249//!     disk)`, exit 14.
250//! 22. Ruling Z24: `-sp` with an archive name that has no directory: zip
251//!     takes the name part as empty, so an answer (a directory, a bare
252//!     name) makes the next split's name from a name shorter than `zip`
253//!     and indexes before its buffer. stuffr takes such a name as one in
254//!     the current directory: `zip -s 64k -sp t.zip big` answered `o/`
255//!     writes `t.z01` here, then `o/t.z02`, ..., `o/t.zip`.
256//! 23. Ruling Z25: `-z` whose first comment line is empty copies `\r\n`
257//!     into a one-byte block (`strcpy(p, "\r\n")`): Ubuntu's build dies in
258//!     glibc's buffer-overflow check (exit 9 through zip's SIGABRT
259//!     handler); Apple's writes the comment. stuffr writes it in both
260//!     flavours, as Apple's build does.
261//! 24. `zipcloak` (crypt.c's `zipcloak()` and `zipbare()`) ignores
262//!     `readlocal()`'s failure (a local header that cannot be found or
263//!     read) and goes on with an uninitialised local header; stuffr ends the
264//!     run there with that failure's code (`zipcloak error: ... (was
265//!     encrypting an entry)` or `(was decrypting an entry)`), the temporary
266//!     removed and the archive untouched. A split archive under `-O` meets
267//!     it at its first entry: zipcloak's `readlocal()` (the UTIL build's)
268//!     looks for every entry in the last part only, warns `Did not find
269//!     entry for <name>`, and Ubuntu's build then dies of SIGSEGV
270//!     (`Interrupted (aborting)`, exit 9, or killed by the signal from its
271//!     handler; measured with a terminal in Docker); stuffr warns the same
272//!     and ends with `Zip file structure invalid`, exit 3. (Without `-O` both
273//!     refuse a split archive: `cannot update a split archive`, exit 16.)
274//!     A central name longer than the local one makes `readlocal()` write
275//!     past the local name's buffer;
276//!     stuffr keeps the local name as it is (a shorter central name cuts it
277//!     with a NUL, as the C does).
278//! 25. `zipnote` and `zipsplit` never set `out_path`, so `readzipfile()`'s
279//!     `strcmp(in_path, out_path)` over a NULL ends in SIGSEGV for an
280//!     archive that does not exist (or is named ``) and for a split archive
281//!     (a `.zip` that says it is one of several): both references' handler
282//!     prints `Interrupted (aborting)` and exits 9. stuffr answers the same
283//!     (`readzip::null_out_crash`); the signal is not raised.
284//! 26. Ruling Z4: Apple's `zipnote -w` closes the input stream twice (the
285//!     second `fclose` closes the temporary's) and always fails, exit 10,
286//!     `Bad file descriptor` then `Temporary file failure`, with the
287//!     archive untouched. That is undefined behaviour; stuffr takes Ubuntu's
288//!     patched answer (patch 07) in both flavours: `-w` works. Pinned by
289//!     `compat_zip_helpers::zipnote_w_is_the_same_in_both_flavours`
290//!     (stuffr's two flavours agree and exit 0) and, on Ubuntu, against the
291//!     reference. A comment's length wraps at 65536 as the C's `ush` does.
292//! 27. `zipsplit`: a `%` in the archive's name is a conversion in the C's
293//!     name template (`sprintf(name, template, n)`, undefined behaviour);
294//!     stuffr takes it as a character. After an error writing the index
295//!     under `-p` and a yes to "redo entire disk?", the C goes back to the
296//!     wrong disk (its
297//!     loop variable holds the failure flag); stuffr redoes disk 1. An
298//!     interrupt removes the parts only when `-b`'s directory is shorter
299//!     than half of `PATH_MAX` (the handler's static holds that much;
300//!     longer arms nothing and the parts stay).
301//! 28. Ubuntu's `UNICODE_SUPPORT` (`unicode.rs`), undefined behaviour of the
302//!     C not reproduced. Where a conversion fails and the C reads through
303//!     the NULL it got (a bit-11 name zip's UTF-8 decoder refuses; a
304//!     Unicode Path field whose UTF-8 name it refuses; `-UN escape` over a
305//!     found name the locale cannot convert) or copies the entry's
306//!     not-yet-set name (a Unicode Path field with no name, read from the
307//!     central directory or by `readlocal`), the reference dies of SIGSEGV
308//!     and its handler answers `Interrupted (aborting)`, exit 9; stuffr
309//!     gives that answer without the crash (`unicode::segv`), as rulings
310//!     Z10/Z13 do. A Unicode Path field shorter than its header says is
311//!     read as zeros past its end (the C reads past it); an old field of
312//!     another length than the new one is taken out and the new one
313//!     appended (the C's copy of what follows it runs four bytes long).
314//!     Patch 12 (`mbstowcs` told the buffer holds `strlen + 1` wide
315//!     characters, which glibc's fortify takes for an overflow and aborts
316//!     on, for every name that is not ASCII) and patch 13 (`-TT`'s command
317//!     buffer one byte short) are Ubuntu's own fixes: both flavours take the
318//!     patched, defined behaviour (Apple has neither code path). Pinned,
319//!     stuffr-only, by `compat_zip_common::ub_rulings`; the crash answers
320//!     are also compared with the reference (`compat_zip_ubuntu`).
321//! 29. `-Z bzip2` (Ubuntu): zipup.c tests whether an entry is text over the
322//!     whole 16 KiB input buffer, past the bytes read, and the buffer is
323//!     allocated once per run and never cleared, so the test sees what an
324//!     earlier entry left there (reproduced: a short text file after a
325//!     longer text file is text). The block's first contents are
326//!     uninitialised in the C; stuffr takes them as zeros (a short first
327//!     text file is binary, `iattr` 0, as measured). Pinned by
328//!     `compat_zip_common::ub_rulings`.
329//! 30. Ruling Z32: an interrupt (SIGINT, SIGTERM) while the new archive
330//!     replaces the old one (`fileio.c:replace`: the archive unlinked, then
331//!     the temporary renamed; or, for a linked archive or a temporary on
332//!     another device such as `-b`'s, the archive truncated and the
333//!     temporary copied into it) makes zip's handler remove the temporary,
334//!     which leaves the archive gone or cut short and no whole copy
335//!     anywhere. stuffr holds both signals back for the length of the
336//!     replace (`zip`, its split parts and `-F`/`-FF`, `zipcloak`, `zipnote
337//!     -w`; `-g`'s restore state is cleared in the same window) and answers
338//!     them as zip does once the archive is whole: `Interrupted
339//!     (aborting)`, exit 9, the new archive in place. A long cross-device
340//!     copy answers Ctrl-C when it ends. When the replace fails after the
341//!     archive was unlinked or cut (ruling Z34), the temporary is the only
342//!     whole copy: it is kept under its own name, where zip's handler would
343//!     remove it. Pinned by
344//!     `tests::an_interrupt_during_replace_leaves_a_whole_archive` and
345//!     `tests::an_interrupt_after_a_failed_replace_keeps_the_temporary`.
346
347use std::collections::BTreeMap;
348use std::ffi::{OsStr, OsString};
349use std::io::{self, BufWriter, Write};
350use std::path::Path;
351use std::process::ExitCode;
352use std::sync::atomic::AtomicU64;
353use std::sync::{Arc, Mutex};
354
355use super::CountedRun;
356use super::host::{self, Host, SANDBOX_OUTPUT_CAP, Sandbox};
357
358pub(crate) mod cli;
359pub(crate) mod fix;
360pub(crate) mod flavour;
361mod messages;
362pub(crate) mod readzip;
363pub(crate) mod scan;
364pub(crate) mod sink;
365pub(crate) mod split;
366pub(crate) mod ttyio;
367pub(crate) mod unicode;
368pub(crate) mod unix;
369pub(crate) mod update;
370pub(crate) mod write;
371pub(crate) mod zipcloak;
372pub(crate) mod zipfile;
373pub(crate) mod zipnote;
374pub(crate) mod zipsplit;
375pub(crate) mod zipup;
376
377pub use flavour::Flavour;
378use messages::{self as m, printf};
379
380/// The environment variable that overrides [`default_flavour`].
381const FLAVOR_VAR: &str = "STUFFR_ZIP_FLAVOR";
382
383/// The exit codes the family can return (`ziperr.h`, the plan's Global
384/// Constraints), for the `compat` fuzz target.
385#[doc(hidden)]
386pub const EXIT_CODES: &[i32] = &[
387    0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19,
388];
389
390/// How a run ends: the exit code (`ziperr()`'s, `EXIT()`'s, or a screen
391/// asked for with exit 0).
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393pub struct Exit {
394    pub code: i32,
395}
396
397/// The flavour a build uses when `STUFFR_ZIP_FLAVOR` is unset or empty:
398/// Apple on macOS and the BSDs, Ubuntu everywhere else, Windows included.
399pub fn default_flavour() -> Flavour {
400    if cfg!(any(
401        target_os = "macos",
402        target_os = "freebsd",
403        target_os = "netbsd",
404        target_os = "openbsd",
405        target_os = "dragonfly"
406    )) {
407        Flavour::Apple
408    } else {
409        Flavour::Ubuntu
410    }
411}
412
413/// The flavour `STUFFR_ZIP_FLAVOR`'s value selects: `apple` or `ubuntu`,
414/// the default when unset or empty, and `Err(value)` for anything else.
415pub(crate) fn flavour_from(value: Option<OsString>) -> Result<Flavour, OsString> {
416    match value {
417        None => Ok(default_flavour()),
418        Some(v) if v.is_empty() => Ok(default_flavour()),
419        Some(v) if v == "apple" => Ok(Flavour::Apple),
420        Some(v) if v == "ubuntu" => Ok(Flavour::Ubuntu),
421        Some(v) => Err(v),
422    }
423}
424
425/// One run's state: zip's globals (`noisy`, `mesg`, `zipfile` and the
426/// rest are in `o` and here), the message layer and the host.
427pub(crate) struct Zr<'a> {
428    /// The process, or a sandbox's stand-ins for it.
429    pub(crate) host: Host,
430    /// Which reference this run behaves as.
431    pub(crate) flavour: Flavour,
432    /// `getenv`: the process's environment, or a sandbox's stand-in.
433    pub(crate) env: &'a dyn Fn(&str) -> Option<OsString>,
434    /// Standard output, flushed after every message.
435    pub(crate) out: BufWriter<Box<dyn Write + Send>>,
436    /// Standard error.
437    pub(crate) err: &'a mut dyn Write,
438    /// The options (zip.c's globals; the helpers read `noisy` here too).
439    pub(crate) o: cli::Options,
440    /// `mesg` is standard error (the archive goes to standard output, or a
441    /// helper keeps standard output for data).
442    pub(crate) mesg_err: bool,
443    /// `mesg_line_started`: a message began a line it has not ended.
444    pub(crate) mesg_line_started: bool,
445    /// The log file (`-lf`), once opened.
446    pub(crate) log: Option<Box<dyn Write>>,
447    /// `logfile_line_started`
448    pub(crate) log_line_started: bool,
449    /// `seen_doubledash`: the first `--` was met.
450    pub(crate) seen_doubledash: bool,
451    /// The program, as `ziperr` of a helper names it.
452    pub(crate) program: &'static str,
453    /// `-g` is writing (zip.c's `tempzip == zipfile`): `ziperr` then says
454    /// it is restoring this archive.
455    pub(crate) restoring: Option<Vec<u8>>,
456    /// zip's `current_in_disk`: the part of a split archive being read.
457    pub(crate) current_in_disk: u64,
458    /// zip's `total_disks`: the parts of the archive being read.
459    pub(crate) total_disks: u64,
460    /// zip's `skip_this_disk`: 1 + the part `-F` was told to skip (0: none).
461    pub(crate) skip_this_disk: u64,
462    /// Standard input as zip's prompts read it (`fgets(..., stdin)`: one
463    /// stdio stream for every prompt of the run), once first asked for.
464    pub(crate) prompt: Option<split::Prompt>,
465    /// `mesg_line_started`, shared with the split writer, which writes its
466    /// questions itself.
467    pub(crate) line_cell: std::rc::Rc<std::cell::Cell<bool>>,
468    /// zip's `using_utf8` (Ubuntu's `UNICODE_SUPPORT`): `en_US.UTF-8`
469    /// loads, so names convert as UTF-8 and new ones get bit 11
470    /// (`unicode::utf8_locale`); always false for Apple.
471    pub(crate) using_utf8: bool,
472}
473
474impl<'a> Zr<'a> {
475    pub(crate) fn new(
476        host: Host,
477        flavour: Flavour,
478        program: &'static str,
479        env: &'a dyn Fn(&str) -> Option<OsString>,
480        err: &'a mut dyn Write,
481    ) -> Zr<'a> {
482        let out = host.out_writer(None);
483        let using_utf8 = flavour::unicode_support(flavour) && unicode::utf8_locale(&host);
484        Zr {
485            host,
486            flavour,
487            env,
488            out,
489            err,
490            o: cli::Options::new(),
491            mesg_err: false,
492            mesg_line_started: false,
493            log: None,
494            log_line_started: false,
495            seen_doubledash: false,
496            program,
497            restoring: None,
498            current_in_disk: 0,
499            total_disks: 1,
500            skip_this_disk: 0,
501            prompt: None,
502            line_cell: std::rc::Rc::new(std::cell::Cell::new(false)),
503            using_utf8,
504        }
505    }
506
507    /// What putlocal/putcentral need of `UNICODE_SUPPORT` (Ubuntu only):
508    /// `using_utf8` and `-UN UTF8`.
509    pub(crate) fn unicode_out(&self) -> Option<(bool, bool)> {
510        flavour::unicode_support(self.flavour).then_some((self.using_utf8, self.o.utf8_force))
511    }
512
513    /// `mesg_line_started = v`, mirrored for the interrupt handler (which
514    /// prints a newline first when a line was started).
515    pub(crate) fn set_line(&mut self, v: bool) {
516        self.mesg_line_started = v;
517        self.line_cell.set(v);
518        host::zip_mesg_state(&self.host, self.mesg_err, v);
519    }
520
521    /// Text written to standard output directly (`printf`, `puts`).
522    pub(crate) fn stdout_write(&mut self, b: &[u8]) {
523        let _ = self.out.write_all(b);
524        let _ = self.out.flush();
525    }
526
527    /// Text written to `mesg` (`fprintf(mesg, ...)` + `fflush`).
528    pub(crate) fn mesg_write(&mut self, b: &[u8]) {
529        if self.mesg_err {
530            let _ = self.err.write_all(b);
531            let _ = self.err.flush();
532        } else {
533            self.stdout_write(b);
534        }
535    }
536
537    /// Text written to the log file, when there is one.
538    pub(crate) fn log_write(&mut self, b: &[u8]) {
539        // A sandbox charges the log's bytes to the output budget, like any
540        // other file; a write the budget cannot hold is lost, as a failed
541        // `fprintf` to the log is (its result is never looked at).
542        if let Host::Sandbox(s) = &self.host
543            && self.log.is_some()
544            && crate::compat::host::take(&s.budget, b.len() as u64).is_err()
545        {
546            return;
547        }
548        if let Some(l) = self.log.as_mut() {
549            let _ = l.write_all(b);
550            let _ = l.flush();
551        }
552    }
553
554    /// zip.c:zipwarn: `\tzip warning: a b` to `mesg` (when noisy) and to
555    /// the log.
556    pub(crate) fn zipwarn(&mut self, a: &[u8], b: &[u8]) {
557        if self.program != "zip" {
558            // the helpers' zipwarn: `zipcloak warning: a b`, always
559            let mut line = Vec::new();
560            printf(
561                &mut line,
562                m::HELPER_WARNING,
563                &[self.program.as_bytes(), a, b],
564            );
565            self.mesg_write(&line);
566            return;
567        }
568        let mut line = Vec::new();
569        printf(&mut line, m::ZIP_WARNING, &[a, b]);
570        if self.o.noisy {
571            if self.mesg_line_started {
572                self.mesg_write(b"\n");
573            }
574            self.mesg_write(&line);
575            self.set_line(false);
576        }
577        if self.log.is_some() {
578            if self.log_line_started {
579                self.log_write(b"\n");
580            }
581            self.log_write(&line);
582            self.log_line_started = false;
583        }
584    }
585
586    /// zip.c:zipmessage: `a` and `b` as one line to `mesg` (when noisy) and
587    /// the log.
588    pub(crate) fn zipmessage(&mut self, a: &[u8], b: &[u8]) {
589        let mut line = a.to_vec();
590        line.extend_from_slice(b);
591        line.push(b'\n');
592        if self.o.noisy {
593            if self.mesg_line_started {
594                self.mesg_write(b"\n");
595            }
596            self.mesg_write(&line);
597            self.set_line(false);
598        }
599        if self.log.is_some() {
600            if self.log_line_started {
601                self.log_write(b"\n");
602            }
603            self.log_write(&line);
604            self.log_line_started = false;
605        }
606    }
607
608    /// zip.c:zipmessage_nl: `a` to `mesg` (when noisy) and the log, then,
609    /// with `nl`, a newline if a line was started.
610    // port: zip.c:zipmessage_nl
611    pub(crate) fn zipmessage_nl(&mut self, a: &[u8], nl: bool) {
612        if self.o.noisy {
613            if !a.is_empty() {
614                self.mesg_write(a);
615                self.set_line(true);
616            }
617            if nl && self.mesg_line_started {
618                self.mesg_write(b"\n");
619                self.set_line(false);
620            }
621        }
622        if self.log.is_some() {
623            if !a.is_empty() {
624                self.log_write(a);
625                self.log_line_started = true;
626            }
627            if nl && self.log_line_started {
628                self.log_write(b"\n");
629                self.log_line_started = false;
630            }
631        }
632    }
633
634    /// zip.c:ziperr: the message for `code` and `h`, to `mesg` and the log;
635    /// the exit it ends the run with is returned.
636    // port: zip.c:ziperr
637    pub(crate) fn ziperr(&mut self, code: i32, h: &[u8]) -> Exit {
638        self.ziperr_with(code, h, None)
639    }
640
641    /// [`Zr::ziperr`] for a code whose message also says why the system
642    /// refused (`ZE_S_PERR`: `zip I/O error: <strerror>`).
643    pub(crate) fn ziperr_os(&mut self, code: i32, h: &[u8], e: &io::Error) -> Exit {
644        self.ziperr_with(code, h, Some(host::strerror(e)))
645    }
646
647    fn ziperr_with(&mut self, code: i32, h: &[u8], why: Option<Vec<u8>>) -> Exit {
648        if self.program != "zip" {
649            // the helpers' ziperr: `perror("zipcloak error")`, then the line
650            if let Some(why) = &why {
651                let mut l = Vec::new();
652                printf(&mut l, m::HELPER_PERROR, &[self.program.as_bytes(), why]);
653                let _ = self.err.write_all(&l);
654                let _ = self.err.flush();
655            }
656            return self.helper_err(code, h);
657        }
658        if self.mesg_line_started {
659            self.mesg_write(b"\n");
660            self.set_line(false);
661        }
662        if self.log.is_some() && self.log_line_started {
663            self.log_write(b"\n");
664            self.log_line_started = false;
665        }
666        let text = usize::try_from(code)
667            .ok()
668            .and_then(|c| m::ZIPERRORS.get(c))
669            .copied()
670            .unwrap_or("");
671        let mut line = Vec::new();
672        printf(&mut line, m::ZIP_ERROR, &[text.as_bytes(), h]);
673        let mut io_line = Vec::new();
674        if let Some(why) = &why {
675            printf(&mut io_line, m::ZIP_IO_ERROR, &[why]);
676            self.mesg_write(&io_line);
677        }
678        self.mesg_write(&line);
679        if self.log.is_some() {
680            if why.is_some() {
681                io_line.push(b'\n');
682                self.log_write(&io_line);
683            }
684            self.log_write(&line);
685            self.log_line_started = false;
686        }
687        if let Some(name) = self.restoring.take() {
688            // -g: the C rewrites the old central directory in place;
689            // stuffr's archive was never touched (deviation 13)
690            let mut l = Vec::new();
691            printf(&mut l, m::ATTEMPTING_RESTORE, &[&name]);
692            self.mesg_write(&l);
693            self.log_write(&l);
694        }
695        Exit { code }
696    }
697
698    /// The helpers' `ziperr` (`zipcloak error: text (h)`, to `mesg`).
699    pub(crate) fn helper_err(&mut self, code: i32, h: &[u8]) -> Exit {
700        let text = usize::try_from(code)
701            .ok()
702            .and_then(|c| m::ZIPERRORS.get(c))
703            .copied()
704            .unwrap_or("");
705        let mut line = Vec::new();
706        printf(
707            &mut line,
708            m::HELPER_ERROR,
709            &[self.program.as_bytes(), text.as_bytes(), h],
710        );
711        self.mesg_write(&line);
712        Exit { code }
713    }
714}
715
716/// A broken-down local time (`struct tm`, the fields zip reads).
717#[derive(Debug, Clone, Copy, PartialEq, Eq)]
718pub(crate) struct Tm {
719    /// The full year (`tm_year + 1900`).
720    pub(crate) year: i64,
721    /// 0 to 11.
722    pub(crate) mon: u32,
723    pub(crate) mday: u32,
724    pub(crate) hour: u32,
725    pub(crate) min: u32,
726    pub(crate) sec: u32,
727    /// 0 is Sunday.
728    pub(crate) wday: u32,
729}
730
731/// `localtime(&t)`: the process's zone under `Host::Real` on unix, UTC for a
732/// sandbox (and for any other target).
733pub(crate) fn localtime(host: &Host, t: i64) -> Tm {
734    #[cfg(unix)]
735    if matches!(host, Host::Real) {
736        // SAFETY: `tm` is plain data that a successful `localtime_r` fills
737        // in whole; both pointers are valid for the call.
738        let tt = t as libc::time_t;
739        let mut tm: libc::tm = unsafe { std::mem::zeroed() };
740        if !unsafe { libc::localtime_r(&tt, &mut tm) }.is_null() {
741            return Tm {
742                year: i64::from(tm.tm_year) + 1900,
743                mon: tm.tm_mon as u32 % 12,
744                mday: tm.tm_mday as u32,
745                hour: tm.tm_hour as u32,
746                min: tm.tm_min as u32,
747                sec: tm.tm_sec as u32,
748                wday: tm.tm_wday as u32 % 7,
749            };
750        }
751    }
752    let _ = host;
753    // UTC, from the civil calendar
754    let days = t.div_euclid(86_400);
755    let secs = t.rem_euclid(86_400);
756    let z = days + 719_468;
757    let era = z.div_euclid(146_097);
758    let doe = z - era * 146_097;
759    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
760    let y = yoe + era * 400;
761    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
762    let mp = (5 * doy + 2) / 153;
763    let d = doy - (153 * mp + 2) / 5 + 1;
764    let mo = if mp < 10 { mp + 3 } else { mp - 9 };
765    let year = if mo <= 2 { y + 1 } else { y };
766    Tm {
767        year,
768        mon: (mo - 1) as u32,
769        mday: d as u32,
770        hour: (secs / 3600) as u32,
771        min: (secs / 60 % 60) as u32,
772        sec: (secs % 60) as u32,
773        wday: ((days + 4).rem_euclid(7)) as u32,
774    }
775}
776
777/// `asctime(localtime(t))` with its newline: `Sun Sep 16 01:03:52 1973\n`.
778/// A sandbox's zone is UTC; so is any other target's.
779pub(crate) fn asctime(host: &Host, t: i64) -> String {
780    const DAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
781    const MONTHS: [&str; 12] = [
782        "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
783    ];
784    let tm = localtime(host, t);
785    format!(
786        "{} {} {:2} {:02}:{:02}:{:02} {}\n",
787        DAYS[tm.wday as usize], MONTHS[tm.mon as usize], tm.mday, tm.hour, tm.min, tm.sec, tm.year
788    )
789}
790
791/// The current time as [`asctime`] shows it.
792pub(crate) fn asctime_now(z: &Zr) -> String {
793    let t = std::time::SystemTime::now()
794        .duration_since(std::time::UNIX_EPOCH)
795        .map_or(0, |d| d.as_secs() as i64);
796    asctime(&z.host, t)
797}
798
799/// Entry point for `zip`, `zipcloak`, `zipnote` and `zipsplit`; `name` is
800/// the canonical invoked name, `argv0` the program as invoked (`-sc` shows
801/// it) and `args` the arguments after it.
802pub fn run(name: &'static str, argv0: &OsStr, args: Vec<OsString>) -> ExitCode {
803    // Deviation 1: as the other families.
804    host::reset_sigpipe();
805    let env = |k: &str| std::env::var_os(k);
806    let mut err = io::stderr();
807    let flavour = match flavour_from(env(FLAVOR_VAR)) {
808        Ok(f) => f,
809        Err(v) => {
810            printf(
811                &mut err,
812                m::FLAVOR_REFUSED,
813                &[name.as_bytes(), v.as_encoded_bytes()],
814            );
815            return ExitCode::from(1);
816        }
817    };
818    let argv0 = if argv0.is_empty() {
819        name.as_bytes()
820    } else {
821        argv0.as_encoded_bytes()
822    };
823    let code = main(flavour, name, argv0, &args, &env, Host::Real, &mut err);
824    // The exit status is the code's low byte, as `exit()` makes it (every
825    // code here is below 256).
826    ExitCode::from(code as u8)
827}
828
829/// [`run`] without the process: operands resolve against `dir`, standard
830/// input is `stdin` (a pipe of bytes), standard output and error are
831/// captured and returned with the exit code, neither is a terminal, the
832/// environment is not read (`ZIP` and `ZIPOPT` are unset;
833/// `STUFFR_ZIP_FLAVOR` is not consulted), no signal is touched, and
834/// standard output and output files share the sandbox's output budget.
835/// For the fuzz target and the tests; not a stable interface.
836#[doc(hidden)]
837pub fn run_in(
838    dir: &Path,
839    name: &str,
840    args: Vec<OsString>,
841    stdin: &[u8],
842    flavour: Flavour,
843) -> (i32, Vec<u8>, Vec<u8>) {
844    let (code, out, err, _) = run_in_counted(flavour, dir, name, &args, stdin);
845    (code, out, err)
846}
847
848/// [`run_in`] in the shape the other families' sandboxed entries have, also
849/// returning what each input read (empty until a task counts them).
850#[doc(hidden)]
851pub fn run_in_counted(
852    flavour: Flavour,
853    dir: &Path,
854    name: &str,
855    args: &[OsString],
856    stdin: &[u8],
857) -> CountedRun {
858    let name: &'static str = match name {
859        "zipcloak" => "zipcloak",
860        "zipnote" => "zipnote",
861        "zipsplit" => "zipsplit",
862        _ => "zip",
863    };
864    let stdout = Arc::new(Mutex::new(Vec::new()));
865    let reads = Arc::new(Mutex::new(BTreeMap::new()));
866    let host = Host::Sandbox(Sandbox {
867        dir: dir.to_path_buf(),
868        stdin: Some(stdin.to_vec()),
869        stdout: stdout.clone(),
870        budget: Arc::new(AtomicU64::new(SANDBOX_OUTPUT_CAP)),
871        reads: reads.clone(),
872    });
873    let mut err = Vec::new();
874    let env = |_: &str| None;
875    let code = main(flavour, name, name.as_bytes(), args, &env, host, &mut err);
876    let out = std::mem::take(&mut *stdout.lock().unwrap_or_else(|p| p.into_inner()));
877    let reads = std::mem::take(&mut *reads.lock().unwrap_or_else(|p| p.into_inner()));
878    (code, out, err, reads)
879}
880
881/// The run below the process boundary. Returns the exit code.
882fn main(
883    flavour: Flavour,
884    name: &'static str,
885    argv0: &[u8],
886    args: &[OsString],
887    env: &dyn Fn(&str) -> Option<OsString>,
888    host: Host,
889    err: &mut dyn Write,
890) -> i32 {
891    let mut z = Zr::new(host, flavour, name, env, err);
892    let args: Vec<Vec<u8>> = args.iter().map(|a| a.as_encoded_bytes().to_vec()).collect();
893    if name == "zipcloak" {
894        let code = match cli::parse_cloak(&mut z, argv0, &args) {
895            Err(Exit { code }) => code,
896            Ok(o) => zipcloak::run(&mut z, o).code,
897        };
898        let _ = z.out.flush();
899        return code;
900    }
901    if name == "zipnote" {
902        let code = match cli::parse_note(&mut z, &args) {
903            Err(Exit { code }) => code,
904            Ok(o) => zipnote::run(&mut z, o).code,
905        };
906        let _ = z.out.flush();
907        return code;
908    }
909    if name == "zipsplit" {
910        let code = match cli::parse_split(&mut z, &args) {
911            Err(Exit { code }) => code,
912            Ok(o) => zipsplit::run(&mut z, o).code,
913        };
914        let _ = z.out.flush();
915        return code;
916    }
917    let code = match cli::parse_zip(&mut z, argv0, &args) {
918        Err(Exit { code }) => code,
919        // A `zip` command line the checks accepted: the names and the archive.
920        Ok(()) => scan::run(&mut z).code,
921    };
922    let _ = z.out.flush();
923    code
924}
925
926#[cfg(test)]
927mod tests;