stuffr_cli/compat/unzip/flavour.rs
1//! The two builds the unzip family can be, and one function per place their
2//! behaviour differs (R13).
3//!
4//! The port follows Apple's tree (`zip-33`'s `unzip60/`); Ubuntu's build
5//! (`6.0-28ubuntu4.1`) differs by its `DEFINES` (`debian/rules`) and its
6//! patch series (`debian/patches/series`). Each difference the front end
7//! meets is a function here, named for its define or patch, so the call
8//! site reads as the C's `#ifdef` did.
9//!
10//! **R13, the order of the work:** Tasks 7–12 give each hook Apple's answer
11//! for both flavours (a pass-through), Task 13 fills the `DEFINES`-driven
12//! ones and Task 14 the patch-driven ones. A hook still passing through says
13//! so in its doc. The banner and compile line are the exception: they are
14//! text, recorded from each reference (`// probe:`), so both are exact now.
15//!
16//! Ubuntu's `DEFINES` (`debian/rules`), each where it acts: `ACORN_FTYPE_NFS`
17//! [`acorn_ftype_nfs`], `WILD_STOP_AT_DIR` [`wild_stop_at_dir`],
18//! `UNICODE_SUPPORT`, `UNICODE_WCHAR` and `UTF8_MAYBE_NATIVE`
19//! [`unicode_support`] (with `Uz::native_is_utf8` and
20//! `Uz::unicode_escape_all`), `NO_LCHMOD` [`no_lchmod`],
21//! `DATE_FORMAT=DF_YMD` [`date_format`], `USE_BZIP2` [`use_bzip2`],
22//! `IZ_HAVE_UXUIDGID` [`iz_have_uxuidgid`], `NO_WORKING_ISPRINT`
23//! [`no_working_isprint`]. Two have no hook: `LARGE_FILE_SUPPORT` is in
24//! both builds (`-v` lists it in both, and the port's sizes and offsets are
25//! 64-bit throughout), and `NOMEMCPY` changes only how inflate.c and
26//! explode.c copy a match within the window (a byte loop instead of
27//! `memcpy()` for non-overlapping runs), which writes the same bytes: no
28//! observable effect (Tasks 4 and 5 probed their C drivers built both ways,
29//! identical output).
30//!
31//! Ubuntu's patch series (`debian/patches/series`), in its order, each
32//! where it acts (`// ubuntu: <patch>` at the site) or why it has none:
33//! - `01-manpages-in-section-1-not-in-section-1l`: man pages only.
34//! - `02-this-is-debian-unzip`: the banner, [`unzip_banner`] and
35//! [`unzip_banner_rest`] (deviation 2's form of it).
36//! - `03-include-unistd-for-kfreebsd`: a build fix (an `#include`); Apple's
37//! tree has it too.
38//! - `04-handle-pkware-verification-bit`, `06-initialize-the-symlink-flag`,
39//! `07-increase-size-of-cfactorstr`, `08-allow-greater-hostver-values`,
40//! `09-cve-2014-8139-crc-overflow`, `10-cve-2014-8140-test-compr-eb`,
41//! `11-cve-2014-8141-getzip64data`: already in Apple's tree in the same
42//! form, so ported for both flavours by Tasks 8-10 (`process.rs`,
43//! `list.rs`, `zipinfo/short.rs`, `extract/extra.rs`).
44//! - `05-fix-uid-gid-handling`: code only `IZ_HAVE_UXUIDGID` builds, ported
45//! in its patched form (`process::ef_scan_for_izux_owner`).
46//! - `12-cve-2014-9636-test-compr-eb`: [`cve_2014_9636_stored_size`].
47//! - `13-remove-build-date`: [`compiled_with`].
48//! - `14-cve-2015-7696`: [`cve_2015_7696_short_header`].
49//! - `15-cve-2015-7697`, `25-cve-2019-13232-fix-bug-in-uzbunzip2`: in
50//! `UZbunzip2()`, which only Ubuntu builds (`extract::member::bunzip2`,
51//! Task 13).
52//! - `16-fix-integer-underflow-csiz-decrypted`: [`csize_below_header_refused`].
53//! - `17-restore-unix-timestamps-accurately`: [`izunix_keeps_times`].
54//! - `18-cve-2014-9913-unzip-buffer-overflow`,
55//! `19-cve-2016-9844-zipinfo-buffer-overflow`: both flavours print the
56//! patched form, Apple's being undefined behaviour (rulings T8-1, T9-2).
57//! - `20-unzip60-alt-iconv-utf8`: [`iconv_charsets`] (`-O`/`-I`,
58//! `unix::charset`, deviation 5).
59//! - `20-cve-2018-1000035-unzip-buffer-overflow`: the password prompt's
60//! length check (`fileio::passw_prompt`, ruling T11-5).
61//! - `21-fix-warning-messages-on-big-files`: [`makeint64_reads_byte_5`].
62//! - `22-cve-2019-13232-fix-bug-in-undefer-input`:
63//! [`cve_2019_13232_undefer_input`].
64//! - `23-cve-2019-13232-zip-bomb-with-overlapped-entries`:
65//! [`detects_overlapped_entries`] (`extract::cover`, `PK_BOMB`) and
66//! [`skips_data_descriptor`].
67//! - `24-cve-2019-13232-do-not-raise-alert-for-misplaced-central-directory`:
68//! the end records' spans (`process::Ecrec::ec_start` and its siblings),
69//! read by [`detects_overlapped_entries`]'s cover.
70//! - `26-cve-2019-13232-fix-bug-in-uzinflate`: in `UZinflate()`, the
71//! `USE_ZLIB` path Ubuntu does not build (its `DEFINES` have no
72//! `USE_ZLIB`): no effect.
73//! - `27-zipgrep-avoid-test-errors`: the `zipgrep` script only, which this
74//! family does not provide.
75//! - `28-cve-2022-0529-and-cve-2022-0530`, `CVE-2021-4217`: code only
76//! `UNICODE_SUPPORT` builds, ported in its patched form
77//! (`process::unicode`, `fileio::unicode_name`, Task 13).
78//! - `handle_windows_zip64`: [`handle_windows_zip64`].
79//! - `29-fix-troff-warning`: man pages only.
80
81/// Which reference the family behaves as.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum Flavour {
84 /// Apple's `/usr/bin/unzip` (UnZip 6.00 with Apple's changes): the
85 /// default on macOS and the BSDs.
86 Apple,
87 /// Ubuntu 24.04's apt `unzip` `6.0-28ubuntu4.1`: the default elsewhere.
88 Ubuntu,
89}
90
91/// `-DUNICODE_SUPPORT` (with `-DUNICODE_WCHAR -DUTF8_MAYBE_NATIVE`), Ubuntu
92/// only: `-U` counts (`uO.U_flag`) instead of being the obsolete inverse of
93/// `-L`, zipinfo accepts `-U`, the usage gains the `-U`/`-UU` line and `-v`
94/// the `UNICODE_SUPPORT` line; names are read whole (`G.filename_full`),
95/// general-purpose bit 11 is noted, and an extra field's UTF-8 name (bit
96/// 11's, or the Unicode Path block `0x7075`) replaces the entry's
97/// (`fileio::unicode_name`).
98pub(crate) fn unicode_support(f: Flavour) -> bool {
99 f == Flavour::Ubuntu
100}
101
102/// `-DACORN_FTYPE_NFS`, Ubuntu only: unzip accepts `-F`
103/// (`uO.acorn_nfs_ext`), `-v` lists `ACORN_FTYPE_NFS`, and under `-F`
104/// `mapname()` gives a name with a SparkFS block its `,xxx` filetype.
105pub(crate) fn acorn_ftype_nfs(f: Flavour) -> bool {
106 f == Flavour::Ubuntu
107}
108
109/// `-DWILD_STOP_AT_DIR`, Ubuntu only: unzip and zipinfo accept `-W`
110/// (`uO.W_flag`), `-v` lists `WILD_STOP_AT_DIR`, and under `-W` a `*` or
111/// `?` stops at a `/` (`match()`'s `sepc`).
112pub(crate) fn wild_stop_at_dir(f: Flavour) -> bool {
113 f == Flavour::Ubuntu
114}
115
116/// `-DUSE_BZIP2`, Ubuntu only: `-v` lists `USE_BZIP2` with libbzip2's
117/// version, the build can do version 4.6, and method 12 is decoded
118/// (`extract::member`'s `UZbunzip2()`, over `stuffr::bzip2_stream`, R1).
119pub(crate) fn use_bzip2(f: Flavour) -> bool {
120 f == Flavour::Ubuntu
121}
122
123/// `20-unzip60-alt-iconv-utf8.patch`, Ubuntu only: the usage texts gain the
124/// `-O CHARSET` and `-I CHARSET` lines (and unzip's a blank line after
125/// them), `-hh` its three entries, unzip and zipinfo accept `-O`/`-I`
126/// (`Options::oem_cp`, `Options::iso_cp`), `OEM_CP` defaults from the
127/// locale, and names and comments are converted from them through
128/// `iconv(3)` in place of the OEM 850 table (`unix::charset`,
129/// `fileio::ext_ascii_to_native`; deviation 5). Apple's build has no
130/// `-O`/`-I`: a bad option, the usage and exit 10.
131pub(crate) fn iconv_charsets(f: Flavour) -> bool {
132 f == Flavour::Ubuntu
133}
134
135/// `UnzipUsageLine1`'s first line, unzip's banner (usage and `-v`), R2's
136/// text: the reference's own line, by stuffr, marked as altered (Info-ZIP
137/// licence clause 3, deviation 2). `%s` is stuffr's version.
138pub(crate) fn unzip_banner(f: Flavour) -> &'static str {
139 match f {
140 // apple: unzip.c:574 (`by Info-ZIP, with modifications by Apple Inc.`)
141 Flavour::Apple => super::messages::APPLE_UNZIP_BANNER,
142 // ubuntu: 02-this-is-debian-unzip.patch (`by Debian. Original by
143 // Info-ZIP.`)
144 Flavour::Ubuntu => super::messages::UBUNTU_UNZIP_BANNER,
145 }
146}
147
148/// What follows the banner in `UnzipUsageLine1`, through its blank line.
149pub(crate) fn unzip_banner_rest(f: Flavour) -> &'static str {
150 match f {
151 // apple: unzip.c:575
152 Flavour::Apple => super::messages::APPLE_REPORT_BUGS,
153 // ubuntu: 02-this-is-debian-unzip.patch (the bug-report line is
154 // gone; the blank line stays)
155 Flavour::Ubuntu => "\n",
156 }
157}
158
159/// zipinfo's banner (`ZipInfoUsageLine1`'s first line), R2's pattern.
160pub(crate) fn zipinfo_banner(f: Flavour) -> &'static str {
161 match f {
162 Flavour::Apple => super::messages::APPLE_ZIPINFO_BANNER,
163 Flavour::Ubuntu => super::messages::UBUNTU_ZIPINFO_BANNER,
164 }
165}
166
167/// funzip's banner (its usage's first line), R2's pattern.
168pub(crate) fn funzip_banner(f: Flavour) -> &'static str {
169 match f {
170 Flavour::Apple => super::messages::APPLE_FUNZIP_BANNER,
171 Flavour::Ubuntu => super::messages::UBUNTU_FUNZIP_BANNER,
172 }
173}
174
175/// `unix/unix.c:version()`'s line in `-v`: the reference's own, which names
176/// its compiler (and, for Apple, its build date).
177pub(crate) fn compiled_with(f: Flavour) -> &'static str {
178 match f {
179 Flavour::Apple => super::messages::APPLE_COMPILED_WITH,
180 // ubuntu: 13-remove-build-date.patch (no ` on <date>`)
181 Flavour::Ubuntu => super::messages::UBUNTU_COMPILED_WITH,
182 }
183}
184
185/// unzpriv.h's `DATE_FORMAT`: the order the listings print a date in.
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub(crate) enum DateFormat {
188 /// `DF_MDY`, `10-26-91` (unix's default).
189 Mdy,
190 /// `DF_DMY`, `26-10-91`.
191 #[allow(dead_code)]
192 Dmy,
193 /// `DF_YMD`, `91-10-26`.
194 Ymd,
195}
196
197/// `-DDATE_FORMAT=DF_YMD`, Ubuntu only: dates print year first; Apple's
198/// is unix's default, `DF_MDY` (unxcfg.h).
199pub(crate) fn date_format(f: Flavour) -> DateFormat {
200 match f {
201 Flavour::Apple => DateFormat::Mdy,
202 Flavour::Ubuntu => DateFormat::Ymd,
203 }
204}
205
206/// `-DNO_WORKING_ISPRINT`, Ubuntu only: unxcfg.h then leaves
207/// `HAVE_WORKING_ISPRINT` undefined, so `fnfilter()` escapes the bytes
208/// below 32 as `^X` and leaves every other byte as it is, DEL and 0x80 to
209/// 0xff included, in every locale (Apple's `isprint()` makes a byte it
210/// refuses `?`). Nothing else changes: `mapname()` and zipinfo's Mac
211/// type/creator line call `isprint()` themselves.
212/// probe: `unzip -l` and `zipinfo` of `c\x80\xa0\xad\xe9\xff\x7f.txt`
213/// show the bytes raw under `C`, `C.UTF-8`, `en_US.UTF-8`, `en_US.utf8`
214/// and `C.utf8` alike (glibc's `isprint()` refuses all of them in each).
215pub(crate) fn no_working_isprint(f: Flavour) -> bool {
216 f == Flavour::Ubuntu
217}
218
219/// `17-restore-unix-timestamps-accurately.patch`, Ubuntu only: an
220/// `EF_IZUNIX2` (`Ux`) block no longer drops the times an earlier
221/// `EF_IZUNIX`/`EF_PKUNIX` (`UX`) block gave, and an `EF_IZUNIX3` (`ux`)
222/// block no longer drops a `UT` block's: each now clears only an earlier
223/// block's owner (`flags &= 0x0ff`, where upstream's cleared `~0x0ff`).
224pub(crate) fn izunix_keeps_times(f: Flavour) -> bool {
225 f == Flavour::Ubuntu
226}
227
228/// `21-fix-warning-messages-on-big-files.patch`, Ubuntu only:
229/// `makeint64()` reads byte 5 of a 64-bit field, which upstream's (and
230/// Apple's) leaves out.
231pub(crate) fn makeint64_reads_byte_5(f: Flavour) -> bool {
232 f == Flavour::Ubuntu
233}
234
235/// `handle_windows_zip64.patch`, Ubuntu only: `find_ecrec64()` takes a
236/// Zip64 locator whose disk count is 0 (as Windows writes it) as no
237/// contradiction of the classic record's disk number.
238pub(crate) fn handle_windows_zip64(f: Flavour) -> bool {
239 f == Flavour::Ubuntu
240}
241
242/// unzpriv.h's `FILNAMSIZ`, the platform's `PATH_MAX`: the longest name
243/// (with its NUL) `G.filename` holds. Not a define or a patch but each
244/// reference's platform constant, so both are exact now: macOS's 1024,
245/// Linux's 4096.
246pub(crate) fn filnamsiz(f: Flavour) -> usize {
247 match f {
248 Flavour::Apple => 1024,
249 Flavour::Ubuntu => 4096,
250 }
251}
252
253/// fileio.c:dos_to_unix_time()'s `BSD4_4` branch: an MS-DOS time is local
254/// time less `localtime()`'s `tm_gmtoff`. Not a define or a patch but each
255/// reference's platform (macOS's is a BSD4_4 one, Linux's is not), so both
256/// are exact: the Linux build adds `timezone` and takes an hour off in
257/// summer time.
258pub(crate) fn dos_time_bsd4_4(f: Flavour) -> bool {
259 f == Flavour::Apple
260}
261
262// ---- extraction (Task 10) ----
263//
264// ruling: T10-1. Apple's own changes to upstream's extraction code (its
265// `__APPLE__` blocks in extract.c, fileio.c and unix.c) are the Ubuntu
266// flavour's upstream code the other way round: no define and no patch
267// carries them, so no later task fills them, and these hooks give each
268// flavour its own tree's answer now (not R13 pass-throughs).
269
270/// apple: extract.c:370, unix.c:1161 (`G.rootpath = realpath(...)`): the
271/// extraction root is always set (`-d` or `.`), held as its real absolute
272/// path without a trailing `/`, and names are shown relative to it after
273/// `uO.exdir` (`FnFilterP`); a directory in the path that is a symlink is
274/// left for the beneath walk, and a parent that will not open is "cannot
275/// enter". Upstream (Ubuntu) sets a root only for `-d`, as the operand plus
276/// `/`, and shows the whole built path (`FnFilter1(G.filename)`).
277pub(crate) fn apple_root(f: Flavour) -> bool {
278 f == Flavour::Apple
279}
280
281/// apple: extract.c:1984 (`error = close_outfile(__G)`): the decoder's code
282/// is replaced by what `close_outfile()` returns. Upstream keeps it.
283/// ruling: T10-2. Apple's `close_outfile()` has no `return` on its
284/// regular-file path (undefined behaviour); its binary answers as if it
285/// returned 0: probe: `unzip bad.zip` of a corrupt deflate entry prints
286/// `error: invalid compressed data to inflate` then ` bad CRC 00000000
287/// (should be ...)` and keeps the file, exit 2. Here it returns `PK_COOL`.
288pub(crate) fn close_outfile_replaces_error(f: Flavour) -> bool {
289 f == Flavour::Apple
290}
291
292/// apple: extract.c:649 (`error = set_deferred_symlink(...)`): a failed
293/// deferred symlink counts in the exit code. Upstream's returns nothing.
294pub(crate) fn deferred_symlink_errors_count(f: Flavour) -> bool {
295 f == Flavour::Apple
296}
297
298/// apple: extract.c:701 (`if (error > error_in_archive)`): a failed
299/// directory attribute raises the exit code to its own; upstream's `if
300/// (!error_in_archive)` only sets it when it was 0.
301pub(crate) fn dir_attrib_error_is_max(f: Flavour) -> bool {
302 f == Flavour::Apple
303}
304
305/// `-DIZ_HAVE_UXUIDGID`, Ubuntu only: `ef_scan_for_izux()` reads the owner
306/// from `ux`/`Ux`/`UX` blocks, so `-X` can restore it.
307pub(crate) fn iz_have_uxuidgid(f: Flavour) -> bool {
308 f == Flavour::Ubuntu
309}
310
311/// `-DNO_LCHMOD`, Ubuntu only: a deferred symlink's mode is not set
312/// (Apple's build calls `lchmod()`). `NO_LCHOWN` stays undefined, so
313/// `SET_SYMLINK_ATTRIBS` is still on: the times and, under `-X`, the owner
314/// are still set.
315pub(crate) fn no_lchmod(f: Flavour) -> bool {
316 f == Flavour::Ubuntu
317}
318
319/// `14-cve-2015-7696.patch`, Ubuntu only: `decrypt()` gives up at the
320/// first `EOF` of an encryption header cut short (`PK_ERR`, "unable to get
321/// password"), returning before `undefer_input()`, so the input stays
322/// deferred for the next entry (Apple reads the missing bytes as `EOF`'s
323/// 0xff and goes on).
324pub(crate) fn cve_2015_7696_short_header(f: Flavour) -> bool {
325 f == Flavour::Ubuntu
326}
327
328/// `16-fix-integer-underflow-csiz-decrypted.patch`, Ubuntu only: an
329/// encrypted stored entry shorter than its header is refused before its
330/// sizes are compared ("invalid compressed data to inflate"), and the rest
331/// of its block of entries with it (`return PK_ERR` from
332/// `extract_or_test_entrylist()`); Apple's size wraps.
333pub(crate) fn csize_below_header_refused(f: Flavour) -> bool {
334 f == Flavour::Ubuntu
335}
336
337/// `12-cve-2014-9636-test-compr-eb.patch`, Ubuntu only: a stored extra
338/// block's data is its size less the method, CRC and size words
339/// (`EB_CMPRHEADLEN` included); Apple's own form of the check
340/// (extract.c:2307) leaves the 6-byte compression header in.
341pub(crate) fn cve_2014_9636_stored_size(f: Flavour) -> bool {
342 f == Flavour::Ubuntu
343}
344
345/// `23-cve-2019-13232-zip-bomb-with-overlapped-entries.patch` with
346/// `24-cve-2019-13232-do-not-raise-alert-for-misplaced-central-
347/// directory.patch`, Ubuntu only: testing or extracting keeps a list of the
348/// spans covered so far (the central directory, a preamble, the end
349/// records, then each entry from its local header to where its reading
350/// stopped, `extract::cover`); an entry starting inside one, or ending over
351/// one, prints "invalid zip file with overlapped components (possible zip
352/// bomb)" and ends the archive, `PK_BOMB` (12). Apple has no bomb check.
353pub(crate) fn detects_overlapped_entries(f: Flavour) -> bool {
354 f == Flavour::Ubuntu
355}
356
357/// `23-cve-2019-13232-zip-bomb-with-overlapped-entries.patch`, Ubuntu
358/// only: after an entry with general-purpose bit 3 is tested or extracted,
359/// its data descriptor is read past (12 bytes, 4 more when it is signed,
360/// 8 more under `G.zip64`); fewer bytes left is `PK_ERR`, silently.
361pub(crate) fn skips_data_descriptor(f: Flavour) -> bool {
362 f == Flavour::Ubuntu
363}
364
365/// `22-cve-2019-13232-fix-bug-in-undefer-input.patch`, Ubuntu only:
366/// `undefer_input()` clamps a negative `G.csize` (a decoder read past the
367/// entry) to 0 before giving the buffer back; Apple's moves `G.inptr` past
368/// the hidden bytes by that much.
369pub(crate) fn cve_2019_13232_undefer_input(f: Flavour) -> bool {
370 f == Flavour::Ubuntu
371}