use std::ffi::OsString;
use std::io::Write;
use crate::compat::host::Host;
mod args;
mod list;
mod messages;
mod run;
mod unlz;
mod zread;
pub(super) fn main(
name: &'static str,
args: &[OsString],
env: &dyn Fn(&str) -> Option<OsString>,
host: &mut Host,
err: &mut dyn Write,
) -> u8 {
let s = match args::parse(name, args, env, err) {
args::Parsed::Exit(code) => return code,
args::Parsed::Run(s) => s,
};
let legacy = env("COMMAND_MODE").is_some_and(|v| v.eq_ignore_ascii_case("legacy"));
run::execute(name, s, name == "zcat" && !legacy, host, err)
}
#[cfg(test)]
mod tests {
use crate::compat::gzip::{Flavour, run_in};
use std::ffi::OsString;
fn scratch(tag: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("stuffr-bsd-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
fn args(a: &[&str]) -> Vec<OsString> {
a.iter().map(OsString::from).collect()
}
fn incompressible() -> Vec<u8> {
let mut x = 0x9e37_79b9_7f4a_7c15u64;
(0..9 << 20)
.map(|_| {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
(x >> 24) as u8
})
.collect()
}
#[test]
fn a_failed_write_is_reported_as_gzip_reports_it() {
let d = scratch("write");
std::fs::write(d.join("big"), incompressible()).unwrap();
std::fs::write(d.join("small"), b"small").unwrap();
let none = |_: &str| None;
let (code, out, err) = run_in(
Flavour::Bsd,
&d,
"gzip",
&args(&["-c", "big", "small"]),
b"",
&none,
);
let err = String::from_utf8(err).unwrap();
assert_eq!(code, 1);
assert_eq!(err, "gzip: write: No space left on device\n".repeat(2));
assert_eq!(out.len() % (64 * 1024), 0, "{}", out.len());
let (code, _, err) = run_in(Flavour::Bsd, &d, "gzip", &args(&["big"]), b"", &none);
let err = String::from_utf8(err).unwrap();
assert_eq!(code, 1);
let lines: Vec<&str> = err.lines().collect();
assert_eq!(lines[0], "gzip: write: No space left on device");
assert!(
lines[1].starts_with("gzip: output file: big.gz wrong size (")
&& lines[1].ends_with(" != 18446744073709551615), deleting"),
"{err}"
);
assert_eq!(lines[2], "gzip: leaving original big");
assert!(d.join("big").exists());
let left: Vec<_> = std::fs::read_dir(&d)
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
assert_eq!(left.len(), 2, "{left:?}");
let _ = std::fs::remove_dir_all(&d);
}
#[cfg(unix)]
#[test]
fn an_output_that_is_the_input_survives_a_failure() {
let d = scratch("self");
let data = incompressible();
std::fs::write(d.join("a.txt.gz"), &data).unwrap();
std::os::unix::fs::symlink("a.txt.gz", d.join("a.txt")).unwrap();
let none = |_: &str| None;
let (code, _, err) = run_in(
Flavour::Bsd,
&d,
"gzip",
&args(&["-f", "a.txt"]),
b"",
&none,
);
let err = String::from_utf8(err).unwrap();
assert_eq!(code, 1);
assert!(
err.starts_with("gzip: write: No space left on device\n")
&& err.ends_with("gzip: leaving original a.txt\n"),
"{err}"
);
assert!(
std::fs::read(d.join("a.txt.gz")).unwrap() == data,
"the input was lost"
);
let left = std::fs::read_dir(&d).unwrap().count();
assert_eq!(left, 2, "a temporary file was left");
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn a_test_of_a_bomb_is_bounded() {
use std::io::Write;
let d = scratch("bomb");
let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
e.write_all(&vec![0u8; 20 << 20]).unwrap();
std::fs::write(d.join("z.gz"), e.finish().unwrap()).unwrap();
let none = |_: &str| None;
let (code, out, err) = run_in(Flavour::Bsd, &d, "gzip", &args(&["-t", "z.gz"]), b"", &none);
assert_eq!(code, 1);
assert!(out.is_empty());
assert_eq!(
String::from_utf8(err).unwrap(),
"gzip: error writing to output: No space left on device\ngzip: z.gz: uncompress failed\n"
);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn an_lzip_dictionary_is_bounded_in_the_sandbox() {
let d = scratch("lzdict");
let codec = stuffr::registry()
.codec(stuffr::FormatId::new("lzip"))
.unwrap();
let buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
struct Shared(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
impl std::io::Write for Shared {
fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(b);
Ok(b.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut sink = codec
.encoder(
Box::new(Shared(buf.clone())),
&stuffr::EncodeOpts::default(),
)
.unwrap();
sink.write_all(&vec![7u8; 12 << 20]).unwrap();
sink.finish().unwrap();
let mut lz = buf.lock().unwrap().clone();
lz[5] = 29; std::fs::write(d.join("z.lz"), lz).unwrap();
let none = |_: &str| None;
let (code, out, err) = run_in(
Flavour::Bsd,
&d,
"gzip",
&args(&["-dc", "z.lz"]),
b"",
&none,
);
assert_eq!(code, 1);
assert!(out.is_empty());
assert_eq!(
String::from_utf8(err).unwrap(),
"gzip: z.lz: uncompress failed\n"
);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn hostile_inputs_never_panic() {
let rounds: usize = std::env::var("STUFFR_BSD_SMOKE")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(150);
let d = scratch("smoke");
let plain: Vec<u8> = (0..3000u32)
.flat_map(|i| format!("{i} smoke ").into_bytes())
.collect();
let encode = |id: &'static str| {
let codec = stuffr::registry().codec(stuffr::FormatId::new(id)).unwrap();
let buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
struct Shared(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
impl std::io::Write for Shared {
fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(b);
Ok(b.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut sink = codec
.encoder(
Box::new(Shared(buf.clone())),
&stuffr::EncodeOpts::default(),
)
.unwrap();
sink.write_all(&plain).unwrap();
sink.finish().unwrap();
buf.lock().unwrap().clone()
};
let gz = {
use std::io::Write;
let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
e.write_all(&plain).unwrap();
e.finish().unwrap()
};
let samples: Vec<(&str, Vec<u8>)> = vec![
("a.lz", encode("lzip")),
("a.Z", encode("compress")),
("a.xz", encode("xz")),
("a.bz2", encode("bzip2")),
(
"a.z",
b"\x1f\x1e\x00\x00\x00\x08\x03\x01\x00\x00AB\x80".to_vec(),
),
("a.gz", gz),
];
let mut x = 0x2545_f491_4f6c_dd1du64;
let mut next = move || {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
x
};
let none = |_: &str| None;
let mut runs = 0;
for (name, good) in &samples {
for round in 0..rounds {
let mut b = good.clone();
match round % 3 {
0 | 1 => {
for _ in 0..1 + next() % 4 {
let span = if next() % 2 == 0 {
b.len().min(24)
} else {
b.len()
};
let at = (next() as usize) % span;
b[at] = next() as u8;
}
}
_ => b.truncate((next() as usize) % b.len()),
}
if round % 7 == 0 && b.len() > 12 {
for v in &mut b[4..12] {
*v = next() as u8;
}
}
std::fs::write(d.join(name), &b).unwrap();
for flags in [&["-dc"][..], &["-t"], &["-l"], &["-lt"]] {
let mut a = flags.to_vec();
a.push(name);
let (code, _, _) = run_in(Flavour::Bsd, &d, "gzip", &args(&a), b"", &none);
assert!(matches!(code, 0..=2), "{name} {flags:?}: exit {code}");
runs += 1;
}
let (code, _, _) = run_in(Flavour::Bsd, &d, "gzcat", &[], &b, &none);
assert!(matches!(code, 0..=2), "stdin {name}: exit {code}");
runs += 1;
}
}
assert_eq!(runs, samples.len() * rounds * 5);
let _ = std::fs::remove_dir_all(&d);
}
}