Skip to main content

stuffr_cli/compat/compress/
mod.rs

1//! The compress family (`compress`, `uncompress`): stuffr behaving as the
2//! platform's own `.Z` compressor.
3//!
4//! Two personalities, as the gzip and unzip families have:
5//!
6//! - **Apple** (`apple/`): Apple's `compress` from `file_cmds-487.0.1`,
7//!   ported from its `compress.c` and `zopen.c` (BSD-3-Clause,
8//!   `LICENSE-compress-bsd`). The module doc of `apple/mod.rs` lists the
9//!   behaviours reproduced and the deviations.
10//! - **ncompress** (`ncompress/`): ncompress 5.0 as Ubuntu's `5.0-1`
11//!   builds it, on every platform, ported from its `compress.c` (the
12//!   Unlicense, `LICENSE-ncompress`). The module doc of `ncompress/mod.rs`
13//!   lists the behaviours reproduced and the deviations.
14//!
15//! The flavour is [`default_flavour`] unless `STUFFR_COMPRESS_FLAVOR` says
16//! `apple` or `ncompress`; [`run`] reads the variable once, before any
17//! argument. Anything else in it is refused, exit 1, in stuffr's own words
18//! (neither reference has the variable). `uncompress` decompresses in both
19//! flavours, on every platform (Ubuntu's own `uncompress` is gzip's script,
20//! and its ncompress binary decompresses only as `uncompress.real`).
21
22use std::collections::BTreeMap;
23use std::ffi::OsString;
24use std::io::{self, Write};
25use std::path::Path;
26use std::process::ExitCode;
27use std::sync::atomic::AtomicU64;
28use std::sync::{Arc, Mutex};
29
30use super::CountedRun;
31use super::host::{Host, SANDBOX_OUTPUT_CAP, Sandbox, reset_sigpipe};
32
33mod apple;
34mod flavour;
35mod ncompress;
36#[cfg(test)]
37mod test_scratch;
38
39pub(crate) use flavour::flavour_from;
40pub use flavour::{Flavour, default_flavour};
41
42/// The environment variable that overrides [`default_flavour`].
43const FLAVOR_VAR: &str = "STUFFR_COMPRESS_FLAVOR";
44
45/// Entry point for `compress` and `uncompress`; `name` is the canonical
46/// invoked name and `args` the arguments after `argv[0]`.
47pub fn run(name: &'static str, args: Vec<OsString>) -> ExitCode {
48    // Both references die of SIGPIPE writing to a closed pipe.
49    reset_sigpipe();
50    let mut err = io::stderr();
51    let flavour = match flavour_from(std::env::var_os(FLAVOR_VAR)) {
52        Ok(f) => f,
53        Err(v) => {
54            let _ = writeln!(
55                err,
56                "{name}: {FLAVOR_VAR} must be apple or ncompress, not '{}'",
57                v.to_string_lossy()
58            );
59            return ExitCode::from(1);
60        }
61    };
62    // getopt names the program by its full `argv[0]`.
63    let argv0 = std::env::args_os()
64        .next()
65        .map(|a| a.as_encoded_bytes().to_vec())
66        .unwrap_or_else(|| name.as_bytes().to_vec());
67    let code = main(flavour, name, &argv0, &args, &mut Host::Real, &mut err);
68    ExitCode::from(code)
69}
70
71/// The flavour's `main`, below the process boundary.
72fn main(
73    flavour: Flavour,
74    name: &'static str,
75    argv0: &[u8],
76    args: &[OsString],
77    host: &mut Host,
78    err: &mut dyn Write,
79) -> u8 {
80    match flavour {
81        Flavour::Apple => apple::main(name, argv0, args, host, err),
82        Flavour::Ncompress => ncompress::main(name, args, host, err),
83    }
84}
85
86/// [`run`] without the process, as `flavour`: operands resolve against
87/// `dir`, standard input is `stdin` (read as a pipe), standard output and
88/// error are captured and returned with the exit code and what each input
89/// opened read, neither is a terminal, and `SIGPIPE` is left alone.
90/// Standard output and output files share the [`SANDBOX_OUTPUT_CAP`]
91/// budget. `env` stands in for the environment; neither reference reads a
92/// variable, so it is never asked (`STUFFR_COMPRESS_FLAVOR` is not
93/// consulted either). For the fuzz target; not a stable interface.
94#[doc(hidden)]
95pub fn run_in_counted(
96    flavour: Flavour,
97    dir: &Path,
98    name: &'static str,
99    args: &[OsString],
100    stdin: &[u8],
101    env: &dyn Fn(&str) -> Option<OsString>,
102) -> CountedRun {
103    let _ = env;
104    let stdout = Arc::new(Mutex::new(Vec::new()));
105    let reads = Arc::new(Mutex::new(BTreeMap::new()));
106    let mut host = Host::Sandbox(Sandbox {
107        dir: dir.to_path_buf(),
108        stdin: Some(stdin.to_vec()),
109        stdout: stdout.clone(),
110        budget: Arc::new(AtomicU64::new(SANDBOX_OUTPUT_CAP)),
111        reads: reads.clone(),
112    });
113    let mut err = Vec::new();
114    let code = main(flavour, name, name.as_bytes(), args, &mut host, &mut err);
115    drop(host);
116    let out = std::mem::take(&mut *stdout.lock().unwrap_or_else(|p| p.into_inner()));
117    let reads = std::mem::take(&mut *reads.lock().unwrap_or_else(|p| p.into_inner()));
118    (i32::from(code), out, err, reads)
119}