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