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;