stuffr_cli/compat/bzip2/mod.rs
1//! The `bzip2` / `bunzip2` / `bzcat` family: stuffr behaving as bzip2 1.0.8.
2//!
3//! Ported from bzip2 1.0.8's `bzip2.c`, copyright (C) 1996-2019 Julian R
4//! Seward, under the bzip2 licence reproduced in `LICENSE-bzip2`. Its flag
5//! handling, checks, messages and exit codes are followed line by line; the
6//! compression itself is stuffr's bzip2 codec, whose output is byte-identical
7//! to bzip2's at every level.
8//!
9//! Known deviations from bzip2 1.0.8:
10//!
11//! - `--version`/`-V`/`-L`/`--license` name stuffr and its version on the
12//! licence's first line (the rest is verbatim).
13//! - Verbosity 2 and above (`-vv`) behaves as `-v`: libbzip2's internal
14//! trace (block CRCs, sorting statistics) is not reachable through stuffr's
15//! codec API, and bzip2.c's own extra blank lines at that level only frame
16//! that trace, so they are left out with it.
17//! - Output is written under a temporary name and renamed into place, so an
18//! interrupted run leaves a `.stuffr-bzip2-*.tmp` file where bzip2's signal
19//! handler would have deleted its partial output.
20//! - When the parent ignores `SIGPIPE` (`SIG_IGN` inherited across exec),
21//! bzip2 reports a Broken pipe I/O error and exits 1; stuffr dies of
22//! `SIGPIPE`, because the Rust runtime sets `SIG_IGN` before `main` and the
23//! inherited disposition cannot be read on stable Rust.
24//! - The `perror` text after a cut-short stream reproduces the `errno` that
25//! bzip2 happens to leave behind as measured on macOS; elsewhere it prints
26//! `strerror(0)`.
27
28use std::ffi::OsString;
29use std::io::{self, Write};
30use std::path::Path;
31use std::process::ExitCode;
32use std::sync::atomic::AtomicU64;
33use std::sync::{Arc, Mutex};
34
35use streams::{Host, Sandbox};
36
37mod args;
38mod messages;
39mod run;
40mod streams;
41
42/// Entry point for `bzip2`, `bunzip2` and `bzcat`; `name` is the canonical
43/// invoked name and `args` the arguments after `argv[0]`.
44pub fn run(name: &'static str, args: Vec<OsString>) -> ExitCode {
45 // bzip2 dies of SIGPIPE writing to a closed pipe; Rust ignores the
46 // signal by default, so restore its default action, on this path only.
47 #[cfg(unix)]
48 // SAFETY: setting a signal's disposition to SIG_DFL installs no handler
49 // and touches no Rust state; it is done before any other thread or I/O.
50 unsafe {
51 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
52 }
53 let env = |k: &str| std::env::var_os(k);
54 ExitCode::from(main(name, &args, &env, &mut Host::Real, &mut io::stderr()))
55}
56
57/// bzip2.c's `main` after the program name: the flags, then the run.
58fn main(
59 name: &'static str,
60 args: &[OsString],
61 env: &dyn Fn(&str) -> Option<OsString>,
62 host: &mut Host,
63 err: &mut dyn Write,
64) -> u8 {
65 match args::parse(name, args, env, err) {
66 args::Parsed::Exit(c) => c,
67 args::Parsed::Run(s) => run::execute(name, s, host, err),
68 }
69}
70
71/// The most a [`run_in`] run may write, to standard output and files
72/// together. Past it a write fails with `ENOSPC`, which bzip2 reports as an
73/// I/O error and exit 1, the code it gives a full disk. A bzip2 stream of a
74/// few dozen bytes can expand to tens of MiB, so without a cap a fuzzer's
75/// short input could fill memory or disk; 8 MiB is past any block's
76/// ordinary output (a 900k block of text) and bounds what a bomb can cost.
77/// `-t` writes nothing but decodes just as much (one 50-byte stream is 45 MB
78/// of zeros), so its decoded bytes are charged too: every path a sandboxed
79/// run takes is bounded by this one figure.
80const SANDBOX_OUTPUT_CAP: u64 = 8 * 1024 * 1024;
81
82/// [`run`] without the process: operands resolve against `dir`, standard
83/// input is `stdin` (read as a pipe), standard output and error are
84/// captured and returned with the exit code, neither is a terminal, `$BZIP2`
85/// and `$BZIP` are unset, and `SIGPIPE` is left alone. Output is capped at
86/// [`SANDBOX_OUTPUT_CAP`], and the bytes `-t` decodes (which go nowhere)
87/// are charged to the same cap. For the fuzz target; not a stable interface.
88#[doc(hidden)]
89pub fn run_in(
90 dir: &Path,
91 name: &'static str,
92 args: &[OsString],
93 stdin: &[u8],
94) -> (i32, Vec<u8>, Vec<u8>) {
95 let stdout = Arc::new(Mutex::new(Vec::new()));
96 let mut host = Host::Sandbox(Sandbox {
97 dir: dir.to_path_buf(),
98 stdin: Some(stdin.to_vec()),
99 stdout: stdout.clone(),
100 budget: Arc::new(AtomicU64::new(SANDBOX_OUTPUT_CAP)),
101 });
102 let mut err = Vec::new();
103 let code = main(name, args, &|_| None, &mut host, &mut err);
104 drop(host);
105 let out = std::mem::take(&mut *stdout.lock().unwrap_or_else(|p| p.into_inner()));
106 (i32::from(code), out, err)
107}