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}