Skip to main content

stuffr_cli/compat/xz/
mod.rs

1//! The xz family (`xz`, `unxz`, `xzcat`, `lzma`, `unlzma`, `lzcat`):
2//! stuffr behaving as XZ Utils 5.8.4's `xz`.
3//!
4//! Ported from XZ Utils 5.8.4 `src/xz/` by Lasse Collin and contributors
5//! (0BSD; the licence ships as `LICENSE-xz`), one module per C file:
6//! `args.rs` (args.c, with the parts of coder.c and util.c the options
7//! reach), `options.rs` (options.c, and liblzma's string_conversion.c for
8//! `--filters`), `suffix.rs` (suffix.c), `hardware.rs` (hardware.c, with
9//! tuklib_physmem/tuklib_cpucores), `message.rs` (message.c, mytime.c,
10//! main.c's exit status, tuklib_exit.c; signals.c's handlers are in
11//! `compat/host.rs`), `coder.rs` (coder.c), `file_io.rs` (file_io.c),
12//! `list.rs` (list.c, util.c's number formatting), `index.rs` (liblzma's
13//! file_info.c, index_decoder.c and block_header_decoder.c, in pure Rust
14//! so that `--list` is the same on both tiers) and this module (main.c).
15//! Every string is in `messages.rs`.
16//!
17//! **Names** (R2): all six map to this family, and the mode comes from the
18//! invoked name by args.c's substring rule, so `unxz` decompresses,
19//! `xzcat` and `lzcat` decompress to standard output, and the `lzma` names
20//! use the `.lzma` format.
21//!
22//! **Hardware** (R3): the real process reads the machine (physical memory,
23//! processor threads, resource limits); a sandboxed run ([`run_in_counted`])
24//! sees two threads and 4 GiB, so fuzzing is deterministic, and its decoder
25//! is limited to 256 MiB (Ruling P7).
26//!
27//! Known deviations (the source of truth: the README's "The xz family"
28//! and the spec mirror this list):
29//!
30//! - `--version`'s first line names stuffr (`xz (stuffr X.Y.Z; XZ Utils
31//!   5.8.4 compatible)`); its second line and `--robot --version` are the
32//!   reference's, on both tiers.
33//! - The `-v` line's speed and elapsed fields vary from run to run, as they
34//!   do between two runs of xz.
35//! - The output is written to a temporary file beside it and published
36//!   when complete (`file_io.rs`); an input that is also the output is
37//!   never lost, even where xz would lose it (spec deviation 4).
38//! - The header probe (`coder.rs`): on the C tier it is xz's own call
39//!   (`lzma_code` with no output room), so the output is opened, refused
40//!   or kept exactly as xz does. On the pure tier only: corruption in the
41//!   first LZMA chunk's properties byte or its range-coder start (`.xz`),
42//!   or in the range-coder start (`.lzma`), is said before the output is
43//!   opened, where xz finds it after opening it. Without `-f` the
44//!   corruption is reported where xz reports that the output exists. With
45//!   `-f` an existing output survives where xz removes it.
46//! - Closed standard streams: run with standard input or output closed
47//!   (`xz <&-`, or `xz -c a >&-`), xz says "(stdin): Read error: Bad file
48//!   descriptor" or "(stdout): Write error: Bad file descriptor" and exits
49//!   1; the Rust runtime reopens a closed standard descriptor on
50//!   `/dev/null` before `main`, so stuffr reads an empty input (compressing
51//!   it and exiting 0, or, decompressing, saying "File format not
52//!   recognized"), or writes to nowhere and exits 0.
53//! - A parent that ignores SIGPIPE is not seen (the Rust runtime ignores
54//!   it before `main`): writing to a closed pipe still kills stuffr, where
55//!   xz says nothing and exits 0.
56//! - The `-v` indicator's one-second timer is polled against the clock
57//!   after each coder call rather than set with `alarm(2)` (message.c's
58//!   own way where there is no SIGALRM). The lines and when they appear
59//!   are xz's; SIGALRM, SIGINFO (where the system has it) and SIGUSR1
60//!   still each ask for a line.
61//! - No sandbox: xz's capsicum, pledge and Landlock confinement
62//!   (`sandbox.c`) is not ported. xz 5.8.4 on macOS has none either, and
63//!   nothing it changes elsewhere is observable.
64//! - The memory figures xz prints (`-vv`, `--list`, the memory-limit
65//!   messages) and every decision against a limit come from liblzma's
66//!   formulas modelled on a configure-built xz 5.8.4 for the host (Ruling
67//!   P5), on both tiers.
68//! - Pure tier: a stream that declares a dictionary larger than its data
69//!   needs commits that memory, where xz only reserves it; `-M` bounds it.
70//!   Where the size is declared (`.lzma` with a known size, a `.xz` Block's
71//!   uncompressed size) the window is capped at that size. A sandboxed run
72//!   caps the decoder at 256 MiB (Ruling P7).
73//! - Pure tier: a `.lzma` whose header gives a known size and that also
74//!   carries an end-of-payload marker is refused (liblzma accepts it; no xz
75//!   encoder writes one).
76//! - Pure tier: with threads and a `--block-size` smaller than the
77//!   dictionary, the Block boundaries differ from xz's (for a lone LZMA2
78//!   chain and no `--block-list`, the threaded encoder's Blocks are at
79//!   least the dictionary size).
80//! - Pure tier: the `-v` line's compressed size and ratio are those of
81//!   the pure encoder's output, which is compatible with xz's, not equal
82//!   to it.
83//! - Pure tier: output written before a decode error may be a strict
84//!   prefix of xz's (Ruling P9: `lzma-rust2` drops what its failing call
85//!   had decoded), and `-v`'s figures at that point differ with it; the
86//!   messages, exit code and file tree are xz's. Near the end of a raw,
87//!   `.lzma` or `.lz` stream, corruption can be reported as truncation
88//!   (Ruling P6).
89
90use std::collections::BTreeMap;
91use std::ffi::{OsStr, OsString};
92use std::io::{self, BufRead, BufReader, Read, Write};
93use std::path::Path;
94use std::process::ExitCode;
95use std::sync::atomic::AtomicU64;
96use std::sync::{Arc, Mutex};
97
98use super::CountedRun;
99use super::host::{self, Host, SANDBOX_OUTPUT_CAP, Sandbox, reset_sigpipe, strerror};
100
101pub(crate) mod args;
102mod coder;
103mod file_io;
104pub(crate) mod hardware;
105mod index;
106mod list;
107pub(crate) mod message;
108mod messages;
109pub(crate) mod options;
110pub(crate) mod suffix;
111
112use args::{Args, OpMode};
113use hardware::Hardware;
114use list::ListTotals;
115use message::{Messages, Status, Verbosity, mask_nonprint};
116
117/// Entry point for the six names; `name` is the canonical invoked name,
118/// `argv0` the program as invoked (every message's name, as xz's
119/// `progname`) and `args` the arguments after it.
120pub fn run(name: &'static str, argv0: &OsStr, args: Vec<OsString>) -> ExitCode {
121    // xz dies of SIGPIPE writing to a closed pipe (observed: `xz -dc big.xz
122    // | head -c1` exits 141 in bash). Before any interrupt cleanup is armed
123    // (Ruling P8).
124    reset_sigpipe();
125    let argv0 = if argv0.is_empty() {
126        name.as_bytes()
127    } else {
128        argv0.as_encoded_bytes()
129    };
130    let env = |k: &str| std::env::var_os(k);
131    let mut err = io::stderr();
132    let code = main(
133        name,
134        argv0,
135        &args,
136        &env,
137        &mut Host::Real,
138        Hardware::detect(),
139        &mut err,
140    );
141    ExitCode::from(code)
142}
143
144/// [`run`] without the process: operands resolve against `dir`, standard
145/// input is `stdin` (a pipe), standard output and error are captured and
146/// returned with the exit code, neither is a terminal, `env` stands in for
147/// the environment (`XZ_DEFAULTS`, `XZ_OPT`, `POSIXLY_CORRECT`) and
148/// `SIGPIPE` is left alone. The machine is R3's fixed two threads and
149/// 4 GiB, and the decoder's memory limit is at most 256 MiB whatever the
150/// arguments say (Ruling P7: the pure tier zero-fills a dictionary as it
151/// allocates it, so a fuzzed header must not reach gigabytes). Standard
152/// output and output files share the [`SANDBOX_OUTPUT_CAP`] budget. No
153/// signal handler is installed and the progress clock is frozen, so a
154/// `-v` line never carries a speed or an elapsed time. For the fuzz
155/// target; not a stable interface.
156#[doc(hidden)]
157pub fn run_in_counted(
158    dir: &Path,
159    name: &'static str,
160    args: &[OsString],
161    stdin: &[u8],
162    env: &dyn Fn(&str) -> Option<OsString>,
163) -> CountedRun {
164    let stdout = Arc::new(Mutex::new(Vec::new()));
165    let reads = Arc::new(Mutex::new(BTreeMap::new()));
166    let mut host = Host::Sandbox(Sandbox {
167        dir: dir.to_path_buf(),
168        stdin: Some(stdin.to_vec()),
169        stdout: stdout.clone(),
170        budget: Arc::new(AtomicU64::new(SANDBOX_OUTPUT_CAP)),
171        reads: reads.clone(),
172    });
173    let mut err = Vec::new();
174    let code = main(
175        name,
176        name.as_bytes(),
177        args,
178        env,
179        &mut host,
180        Hardware::SANDBOX,
181        &mut err,
182    );
183    drop(host);
184    let out = std::mem::take(&mut *stdout.lock().unwrap_or_else(|p| p.into_inner()));
185    let reads = std::mem::take(&mut *reads.lock().unwrap_or_else(|p| p.into_inner()));
186    (i32::from(code), out, err, reads)
187}
188
189/// main.c: main, below the process boundary.
190fn main(
191    name: &str,
192    argv0: &[u8],
193    args: &[OsString],
194    env: &dyn Fn(&str) -> Option<OsString>,
195    host: &mut Host,
196    hw: Hardware,
197    err: &mut dyn Write,
198) -> u8 {
199    // message_init: the progress signals' handlers (the real process only).
200    host::install_progress_signals(host);
201    let mut out = host.out_writer(None);
202    let (mut a, mut msg) = match args::parse(name, argv0, env, args, host, hw, &mut out, err) {
203        Ok(parsed) => parsed,
204        Err(code) => return code,
205    };
206    // message_init's progress_automatic, and what message.c reads of the
207    // options.
208    msg.automatic = host.stderr_is_tty();
209    msg.robot = a.robot;
210    msg.list = a.op == OpMode::List;
211    if matches!(host, Host::Sandbox(_)) {
212        msg.clock = message::frozen_clock;
213    }
214    if a.op != OpMode::List && a.robot {
215        return msg.fatal(err, messages::ROBOT_CODING, &[]);
216    }
217    // message_set_files: unknown with --files/--files0.
218    msg.files_total = if a.files_from.is_some() {
219        0
220    } else {
221        a.files.len() as u32
222    };
223    // Refuse to write compressed data to a terminal.
224    if a.op == OpMode::Compress
225        && (a.to_stdout || a.files == [b"-".to_vec()])
226        && is_tty_stdout(host, &mut msg, err)
227    {
228        msg.try_help(err);
229        return Status::Error as u8;
230    }
231    // signals_init(), not under --list: the SIGTSTP handler now; the
232    // cleanup handlers when an output file is armed (host.rs), the only
233    // time they act.
234    if a.op != OpMode::List {
235        host::install_stop_handler(host);
236    }
237    let names = std::mem::take(&mut a.files);
238    let mut totals = ListTotals::default();
239    for n in &names {
240        let mut file = Some(n.as_slice());
241        if n == b"-" {
242            if a.op == OpMode::Compress {
243                if is_tty_stdout(host, &mut msg, err) {
244                    continue;
245                }
246            } else if host.stdin_is_tty() {
247                msg.error(err, messages::TTY_STDIN, &[]);
248                continue;
249            }
250            if a.files_from.as_ref().is_some_and(|f| f.name.is_none()) {
251                msg.error(err, messages::STDIN_BOTH, &[]);
252                continue;
253            }
254            file = None;
255        }
256        if let Err(code) = run_one(&a, file, host, &mut msg, &mut out, err, &mut totals) {
257            return code;
258        }
259    }
260    if let Some(from) = a.files_from.take() {
261        let delim = from.delim;
262        let display = from.display_name().to_vec();
263        let source: Box<dyn Read + '_> = match from.file {
264            Some(f) => Box::new(f),
265            None => Box::new(host.stdin()),
266        };
267        let mut reader = BufReader::new(source);
268        while let Some(name) = read_name(&mut reader, delim, &display, &mut msg, err) {
269            if let Err(code) = run_one(&a, Some(&name), host, &mut msg, &mut out, err, &mut totals)
270            {
271                return code;
272            }
273        }
274    }
275    if a.op == OpMode::List {
276        list::list_totals(&a, &msg, &totals, &mut out);
277    }
278    let status = msg.final_status();
279    message::exit(
280        status,
281        &msg.progname,
282        &mut out,
283        err,
284        msg.verbosity != Verbosity::Silent,
285    )
286}
287
288/// util.c: is_tty_stdout, which reports a terminal as an error.
289fn is_tty_stdout(host: &Host, msg: &mut Messages, err: &mut dyn Write) -> bool {
290    let tty = host.stdout_is_tty();
291    if tty {
292        msg.error(err, messages::TTY_STDOUT, &[]);
293    }
294    tty
295}
296
297/// coder.c: coder_run, or list.c: list_file under `--list`, for one input
298/// (`None`: standard input). `Err` is the exit code of a run list_file
299/// ends (message_fatal).
300fn run_one(
301    a: &Args,
302    name: Option<&[u8]>,
303    host: &mut Host,
304    msg: &mut Messages,
305    out: &mut dyn Write,
306    err: &mut dyn Write,
307    totals: &mut ListTotals,
308) -> Result<(), u8> {
309    if a.op == OpMode::List {
310        return list::list_file(a, name, host, msg, out, err, totals);
311    }
312    coder::run(a, name, host, msg, err);
313    Ok(())
314}
315
316/// main.c: read_name: the next name from `--files`/`--files0`'s source,
317/// skipping empty ones; `None` at the end or after an error (said).
318fn read_name(
319    r: &mut dyn BufRead,
320    delim: u8,
321    display: &[u8],
322    msg: &mut Messages,
323    err: &mut dyn Write,
324) -> Option<Vec<u8>> {
325    let mut name = Vec::new();
326    loop {
327        let mut byte = [0u8; 1];
328        match r.read(&mut byte) {
329            Ok(0) => {
330                if !name.is_empty() {
331                    msg.error(
332                        err,
333                        messages::NAMES_UNEXPECTED_END,
334                        &[&mask_nonprint(display)],
335                    );
336                }
337                return None;
338            }
339            Ok(_) => {}
340            Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
341            Err(e) => {
342                msg.error(
343                    err,
344                    messages::READ_NAMES_FAILED,
345                    &[&mask_nonprint(display), &strerror(&e)],
346                );
347                return None;
348            }
349        }
350        let c = byte[0];
351        if c == delim {
352            if name.is_empty() {
353                continue;
354            }
355            return Some(name);
356        }
357        if c == 0 {
358            msg.error(err, messages::NAMES_NUL, &[&mask_nonprint(display)]);
359            return None;
360        }
361        name.push(c);
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    fn names(data: &[u8], delim: u8) -> (Vec<String>, String, Status) {
370        let mut msg = Messages::new(b"xz");
371        let mut err = Vec::new();
372        let mut r = data;
373        let mut got = Vec::new();
374        while let Some(n) = read_name(&mut r, delim, b"(stdin)", &mut msg, &mut err) {
375            got.push(String::from_utf8(n).unwrap());
376        }
377        (got, String::from_utf8(err).unwrap(), msg.status)
378    }
379
380    #[test]
381    fn read_name_splits_and_skips_empty_names() {
382        assert_eq!(
383            names(b"a\n\nb\n", b'\n'),
384            (vec!["a".into(), "b".into()], String::new(), Status::Success)
385        );
386        assert_eq!(names(b"a\0b\0", 0).0, ["a", "b"]);
387        let (got, err, st) = names(b"a\nb", b'\n');
388        assert_eq!(got, ["a"]);
389        assert_eq!(
390            (err.as_str(), st),
391            (
392                "xz: (stdin): Unexpected end of input when reading filenames\n",
393                Status::Error
394            )
395        );
396        let (got, err, _) = names(b"a\0b\n", b'\n');
397        assert!(got.is_empty());
398        assert!(err.contains("Null character found"), "{err}");
399    }
400
401    /// The coder under `Host::Sandbox`: standard input to standard output
402    /// and back, a file to its `.xz` (the input removed), a test, and
403    /// output past the sandbox's budget failing as a full disk does.
404    #[test]
405    fn run_in_counted_codes_in_the_sandbox() {
406        let dir = std::env::temp_dir().join(format!("stuffr-xz-sandbox-{}", std::process::id()));
407        let _ = std::fs::remove_dir_all(&dir);
408        std::fs::create_dir_all(&dir).unwrap();
409        let plain = b"sandboxed\n".repeat(100);
410        let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
411        let (code, packed, err, _) = run_in_counted(&dir, "xz", &args(&["-T1"]), &plain, &|_| None);
412        assert_eq!((code, err.as_slice()), (0, &b""[..]));
413        assert!(packed.starts_with(&[0xFD, b'7', b'z', b'X', b'Z', 0]));
414        let (code, out, err, _) = run_in_counted(&dir, "xzcat", &[], &packed, &|_| None);
415        assert_eq!((code, out, err), (0, plain.clone(), Vec::new()));
416
417        std::fs::write(dir.join("a"), &plain).unwrap();
418        let (code, _, err, reads) = run_in_counted(&dir, "xz", &args(&["a"]), b"", &|_| None);
419        assert_eq!((code, err), (0, Vec::new()));
420        assert!(!dir.join("a").exists());
421        assert_eq!(
422            reads.get(&dir.join("a")).map(|r| r.bytes),
423            Some(plain.len() as u64)
424        );
425        let (code, out, err, _) =
426            run_in_counted(&dir, "xz", &args(&["-t", "a.xz"]), b"", &|_| None);
427        assert_eq!((code, out, err), (0, Vec::new(), Vec::new()));
428        let (code, _, err, _) = run_in_counted(&dir, "unxz", &args(&["a.xz"]), b"", &|_| None);
429        assert_eq!((code, err), (0, Vec::new()));
430        assert_eq!(std::fs::read(dir.join("a")).unwrap(), plain);
431
432        // 9 MiB of zeros decode past the 8 MiB budget.
433        let zeros = vec![0u8; 9 << 20];
434        let (_, bomb, _, _) = run_in_counted(&dir, "xz", &args(&["-T1"]), &zeros, &|_| None);
435        let (code, _, err, _) = run_in_counted(&dir, "xzcat", &[], &bomb, &|_| None);
436        assert_eq!(code, 1);
437        assert!(
438            String::from_utf8_lossy(&err).starts_with("xzcat: (stdout): Write error: "),
439            "{}",
440            String::from_utf8_lossy(&err)
441        );
442        // A test counts what it decodes against the same budget.
443        let (code, _, err, _) = run_in_counted(&dir, "xz", &args(&["-t"]), &bomb, &|_| None);
444        assert_eq!(code, 1);
445        assert!(
446            String::from_utf8_lossy(&err).starts_with("xz: (stdout): Write error: "),
447            "{}",
448            String::from_utf8_lossy(&err)
449        );
450        let _ = std::fs::remove_dir_all(&dir);
451    }
452
453    /// `-v` under `Host::Sandbox`: standard error is no terminal and the
454    /// clock is frozen, so each file gets its one line, with no speed or
455    /// elapsed time however long the run takes.
456    #[test]
457    fn run_in_counted_verbose_is_deterministic() {
458        let dir = std::env::temp_dir();
459        let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
460        let plain = vec![b'x'; 300_000];
461        let (code, packed, err, _) =
462            run_in_counted(&dir, "xz", &args(&["-v", "-T1", "-0"]), &plain, &|_| None);
463        assert_eq!(code, 0);
464        let ratio = packed.len() as f64 / plain.len() as f64;
465        let want = format!("(stdin): {} B / 293.0 KiB = {ratio:.3}\n", packed.len());
466        assert_eq!(String::from_utf8(err).unwrap(), want);
467        let (code, _, err, _) = run_in_counted(&dir, "xz", &args(&["-tv"]), &packed, &|_| None);
468        assert_eq!((code, String::from_utf8(err).unwrap()), (0, want));
469    }
470
471    /// `--list` under `Host::Sandbox`: a file is listed and its reads
472    /// counted, standard input is refused, and arbitrary bytes end in
473    /// xz's own exit codes.
474    #[test]
475    fn run_in_counted_lists_in_the_sandbox() {
476        let dir = std::env::temp_dir().join(format!("stuffr-xz-list-{}", std::process::id()));
477        let _ = std::fs::remove_dir_all(&dir);
478        std::fs::create_dir_all(&dir).unwrap();
479        let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
480        let (_, packed, _, _) = run_in_counted(&dir, "xz", &args(&["-T1"]), b"listed\n", &|_| None);
481        std::fs::write(dir.join("a.xz"), &packed).unwrap();
482        let (code, out, err, reads) = run_in_counted(
483            &dir,
484            "xz",
485            &args(&["--robot", "-lvv", "a.xz"]),
486            b"",
487            &|_| None,
488        );
489        assert_eq!((code, String::from_utf8(err).unwrap()), (0, String::new()));
490        let out = String::from_utf8(out).unwrap();
491        assert!(out.starts_with("name\ta.xz\nfile\t1\t1\t"), "{out}");
492        // The reference build's LZ_DICT_EXTRA: 32 more bytes on x86 with
493        // SSE2 (memusage.rs, Profile::lz_dict_extra).
494        let needed = if cfg!(any(
495            target_arch = "x86_64",
496            all(target_arch = "x86", target_feature = "sse2")
497        )) {
498            8_454_808
499        } else {
500            8_454_776
501        };
502        assert!(
503            out.ends_with(&format!("\tCRC64\t0\t1\t{needed}\tno\t50000002\n")),
504            "{out}"
505        );
506        let read = reads.get(&dir.join("a.xz")).map_or(0, |r| r.bytes);
507        assert!(read > 0 && read <= 4 * packed.len() as u64, "read {read}");
508        let (code, _, err, _) = run_in_counted(&dir, "xz", &args(&["-l"]), &packed, &|_| None);
509        assert_eq!(code, 1);
510        assert_eq!(
511            String::from_utf8(err).unwrap(),
512            "xz: --list does not support reading from standard input\n"
513        );
514        let short = short_block(4, 5);
515        assert_eq!(short.len(), 44);
516        for (i, junk) in [
517            &b""[..],
518            b"\xfd7zXZ\0\0\x01",
519            &[0u8; 64],
520            &packed[..packed.len() - 4],
521            &short,
522        ]
523        .iter()
524        .enumerate()
525        {
526            let name = format!("j{i}.xz");
527            std::fs::write(dir.join(&name), junk).unwrap();
528            let (code, _, _, _) =
529                run_in_counted(&dir, "xz", &args(&["-lvv", &name]), b"", &|_| None);
530            assert_eq!(code, 1, "{name}");
531        }
532        let _ = std::fs::remove_dir_all(&dir);
533    }
534
535    /// A CRC32 of `data`.
536    fn crc32(data: &[u8]) -> [u8; 4] {
537        let mut c = flate2::Crc::new();
538        c.update(data);
539        c.sum().to_le_bytes()
540    }
541
542    /// A Stream with check `check` of one Block of filler whose Index
543    /// record is (`unpadded`, 1000): a Block as short as its Check (or
544    /// shorter, or a few bytes longer) for `-vv` to read the header of.
545    fn short_block(check: u8, unpadded: u64) -> Vec<u8> {
546        let mut s = vec![0xFD, b'7', b'z', b'X', b'Z', 0, 0, check];
547        s.extend(crc32(&[0, check]));
548        s.extend(std::iter::repeat_n(0x55, ((unpadded + 3) & !3) as usize));
549        let mut index = vec![0, 1];
550        let mut v = unpadded;
551        while v >= 0x80 {
552            index.push(v as u8 | 0x80);
553            v >>= 7;
554        }
555        index.extend([v as u8, 0xE8, 0x07]);
556        while !index.len().is_multiple_of(4) {
557            index.push(0);
558        }
559        let crc = crc32(&index);
560        index.extend(crc);
561        s.extend(&index);
562        let mut tail = ((index.len() / 4 - 1) as u32).to_le_bytes().to_vec();
563        tail.extend([0, check]);
564        s.extend(crc32(&tail));
565        s.extend(tail);
566        s.extend(b"YZ");
567        s
568    }
569
570    /// list.rs's `-vv` paths (Block Headers, Check values) on hostile
571    /// files, not only index.rs's walk: every check ID with Blocks from
572    /// shorter than their Check to a little past it, and every byte of a
573    /// multi-Block file flipped three ways. Never a panic; only xz's exit
574    /// codes.
575    #[test]
576    fn list_vv_on_hostile_input_never_panics() {
577        let dir = std::env::temp_dir().join(format!("stuffr-xz-listvv-{}", std::process::id()));
578        let _ = std::fs::remove_dir_all(&dir);
579        std::fs::create_dir_all(&dir).unwrap();
580        let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
581        let plain = b"hostile listing\n".repeat(40);
582        let (_, packed, _, _) = run_in_counted(
583            &dir,
584            "xz",
585            &args(&["-T1", "-C", "sha256", "--block-size=100"]),
586            &plain,
587            &|_| None,
588        );
589        let mut inputs = Vec::new();
590        for check in 0..16u8 {
591            for unpadded in 5..=72 {
592                inputs.push(short_block(check, unpadded));
593            }
594        }
595        for i in 0..packed.len() {
596            for mask in [0x01, 0x80, 0xFF] {
597                let mut b = packed.clone();
598                b[i] ^= mask;
599                inputs.push(b);
600            }
601        }
602        for (i, data) in inputs.iter().enumerate() {
603            let name = format!("h{i}.xz");
604            std::fs::write(dir.join(&name), data).unwrap();
605            for form in [&["-lvv"][..], &["--robot", "-lvv"]] {
606                let mut a = form.to_vec();
607                a.push(&name);
608                let (code, _, _, _) = run_in_counted(&dir, "xz", &args(&a), b"", &|_| None);
609                assert!(matches!(code, 0 | 1), "{name} {form:?}: exit {code}");
610            }
611            std::fs::remove_file(dir.join(&name)).unwrap();
612        }
613        let _ = std::fs::remove_dir_all(&dir);
614    }
615
616    /// `--files=FILE` opens its list through `Host::track`, so the fuzz
617    /// target's read bound sees the list as well as the names in it.
618    #[test]
619    fn files_from_a_file_are_counted() {
620        let dir = std::env::temp_dir().join(format!("stuffr-xz-files-{}", std::process::id()));
621        let _ = std::fs::remove_dir_all(&dir);
622        std::fs::create_dir_all(&dir).unwrap();
623        std::fs::write(dir.join("list"), b"a\n").unwrap();
624        std::fs::write(dir.join("a"), b"listed by name\n").unwrap();
625        let args = ["-k".into(), "--files=list".into()];
626        let (code, _, err, reads) = run_in_counted(&dir, "xz", &args, b"", &|_| None);
627        assert_eq!((code, String::from_utf8(err).unwrap()), (0, String::new()));
628        let got = |n: &str| reads.get(&dir.join(n)).copied().unwrap_or_default();
629        assert_eq!(
630            (got("list").opens, got("list").bytes, got("list").len),
631            (1, 2, 2)
632        );
633        assert_eq!((got("a").opens, got("a").bytes), (1, 15));
634        let _ = std::fs::remove_dir_all(&dir);
635    }
636
637    #[test]
638    fn run_in_counted_parses_in_the_sandbox() {
639        let dir = std::env::temp_dir();
640        let (code, out, err, reads) = run_in_counted(&dir, "xz", &["-h".into()], b"", &|_| None);
641        assert_eq!((code, err.len(), reads.len()), (0, 0, 0));
642        assert!(out.starts_with(b"Usage: xz [OPTION]... [FILE]...\n"));
643        // R3: the sandbox's machine.
644        let (code, out, _, _) = run_in_counted(
645            &dir,
646            "xz",
647            &["--robot".into(), "--info-memory".into()],
648            b"",
649            &|_| None,
650        );
651        assert_eq!(code, 0);
652        assert_eq!(
653            String::from_utf8(out).unwrap(),
654            "4294967296\t0\t0\t268435456\t1073741824\t2\n"
655        );
656        let (code, _, err, _) = run_in_counted(&dir, "xz", &["--robot".into()], b"", &|_| None);
657        assert_eq!(code, 1);
658        assert_eq!(
659            String::from_utf8(err).unwrap(),
660            "xz: Compression and decompression with --robot are not supported yet.\n"
661        );
662    }
663}