use std::ffi::OsString;
use std::io::Write;
use crate::compat::host::Host;
mod args;
mod lzw;
mod messages;
mod run;
pub(super) fn main(
name: &'static str,
args: &[OsString],
host: &mut Host,
err: &mut dyn Write,
) -> u8 {
use args::Parsed;
let usage = |w: &mut dyn Write| messages::printf(w, messages::USAGE, &[name.as_bytes()]);
let to_stdout = |host: &mut Host, text: &[u8]| {
let mut w = host.out_writer(None);
let _ = w.write_all(text);
let _ = w.flush();
};
match args::parse(name, args) {
Parsed::Run(s) => run::execute(s, host, err),
Parsed::Version => {
to_stdout(host, messages::ABOUT.as_bytes());
0
}
Parsed::Help => {
let mut text = Vec::new();
usage(&mut text);
to_stdout(host, &text);
0
}
Parsed::Unknown(c) => {
messages::printf(err, messages::UNKNOWN_FLAG, &[&[c]]);
usage(err);
1
}
Parsed::MissingMaxbits => {
let _ = err.write_all(messages::MISSING_MAXBITS.as_bytes());
usage(err);
1
}
}
}
#[cfg(test)]
mod tests {
use crate::compat::compress::{Flavour, run_in_counted};
use std::ffi::OsString;
use crate::compat::compress::test_scratch::scratch;
fn run(
d: &std::path::Path,
name: &'static str,
a: &[&str],
stdin: &[u8],
) -> (i32, Vec<u8>, String) {
let args: Vec<OsString> = a.iter().map(OsString::from).collect();
let (code, out, err, _) =
run_in_counted(Flavour::Ncompress, d, name, &args, stdin, &|_| None);
(code, out, String::from_utf8(err).unwrap())
}
fn text(n: usize) -> Vec<u8> {
(0..n).map(|i| b"the quick brown fox "[i % 20]).collect()
}
fn noise(n: usize, mut x: u64) -> Vec<u8> {
(0..n)
.map(|_| {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
(x >> 24) as u8
})
.collect()
}
#[test]
fn files_round_trip_in_the_sandbox() {
let d = scratch("trip");
let data = text(100_000);
std::fs::write(d.join("f"), &data).unwrap();
let args: Vec<OsString> = vec!["-v".into(), "f".into()];
let (code, _, err, reads) =
run_in_counted(Flavour::Ncompress, &d, "compress", &args, b"", &|_| None);
let err = String::from_utf8(err).unwrap();
assert_eq!(code, 0, "{err}");
assert!(
err.starts_with("f: -- replaced with f.Z Compression: "),
"{err}"
);
let f = reads[&d.join("f")];
assert_eq!((f.opens, f.bytes, f.len), (1, 100_000, 100_000));
assert!(!d.join("f").exists());
let (code, _, err) = run(&d, "uncompress", &["f"], b"");
assert_eq!((code, err.as_str()), (0, ""));
assert_eq!(std::fs::read(d.join("f")).unwrap(), data);
assert!(!d.join("f.Z").exists());
}
#[test]
fn usage_and_version() {
let d = scratch("usage");
let (code, out, err) = run(&d, "uncompress", &["-h"], b"");
assert_eq!((code, err.as_str()), (0, ""));
assert!(out.starts_with(b"Usage: uncompress [-dfhvcVr] [-b maxbits] [--] [path ...]\n"));
let (code, out, _) = run(&d, "compress", &["-V"], b"");
assert_eq!(code, 0);
assert!(out.starts_with(b"Compress version: (N)compress 5.0\n"));
let (code, out, err) = run(&d, "compress", &["-z"], b"");
assert_eq!(code, 1);
assert!(out.is_empty());
assert!(
err.starts_with("Unknown flag: 'z'; Usage: compress "),
"{err}"
);
}
#[test]
fn a_failed_write_keeps_the_input() {
let d = scratch("full");
let data = noise(9 << 20, 0x9e37_79b9_7f4a_7c15);
std::fs::write(d.join("big"), &data).unwrap();
let (code, _, err) = run(&d, "compress", &["-f", "big"], b"");
assert_eq!(
(code, err.as_str()),
(1, "\nwrite error onbig.Z: No space left on device\n")
);
assert_eq!(std::fs::read(d.join("big")).unwrap(), data);
assert!(!d.join("big.Z").exists());
let (code, out, err) = run(&d, "compress", &["-c", "big"], b"");
assert_eq!(
(code, err.as_str()),
(1, "\nwrite error onbig.Z: No space left on device\n")
);
assert!(!out.is_empty() && out.len() % super::lzw::OBUFSIZ == 0);
let (code, _, err) = run(&d, "compress", &[], &data);
assert_eq!(
(code, err.as_str()),
(1, "\nwrite error onstdout: No space left on device\n")
);
}
#[test]
fn a_failed_decompressing_write_keeps_the_input() {
let d = scratch("full-d");
let (code, z, err) = run(&d, "compress", &[], &vec![0u8; 9 << 20]);
assert_eq!(code, 0, "{err}");
std::fs::write(d.join("z.Z"), &z).unwrap();
let (code, _, err) = run(&d, "uncompress", &["z.Z"], b"");
assert_eq!(
(code, err.as_str()),
(1, "\nwrite error onz: No space left on device\n")
);
assert_eq!(std::fs::read(d.join("z.Z")).unwrap(), z);
assert!(!d.join("z").exists());
}
#[test]
fn maxbits_9_streams_read_back() {
let d = scratch("n1");
let data = [text(50_000), noise(50_000, 7), vec![0u8; 50_000]].concat();
for v in ["9", "0", "-1", "8", "abc", ""] {
let (code, z, err) = run(&d, "compress", &["-b", v], &data);
assert_eq!((code, z[2]), (0, 0x89), "-b {v:?}: {err}");
let (code, back, err) = run(&d, "uncompress", &[], &z);
assert_eq!((code, err.as_str()), (0, ""), "-b {v:?}");
assert!(back == data, "-b {v:?}");
}
}
#[test]
fn a_refused_input_is_kept_under_f() {
let d = scratch("n4");
std::fs::write(d.join("bad.Z"), b"plain text").unwrap();
let (code, _, err) = run(&d, "uncompress", &["-f", "bad.Z"], b"");
assert_eq!(
(code, err.as_str()),
(1, "bad.Z: not in compressed format\n")
);
assert_eq!(std::fs::read(d.join("bad.Z")).unwrap(), b"plain text");
assert!(!d.join("bad").exists());
std::fs::write(d.join("e.Z"), b"").unwrap();
let (code, _, err) = run(&d, "uncompress", &["-f", "e.Z"], b"");
assert_eq!((code, err.as_str()), (0, ""));
assert!(!d.join("e.Z").exists());
assert_eq!(std::fs::read(d.join("e")).unwrap(), b"");
}
#[test]
fn a_corrupt_input_ends_the_run() {
let d = scratch("corrupt");
std::fs::write(d.join("w.Z"), [0x1f, 0x9d, 0x90, 0xff, 0x03, 0x00]).unwrap();
std::fs::write(d.join("x"), b"x").unwrap();
let (code, _, err) = run(&d, "uncompress", &["w.Z", "x"], b"");
assert_eq!(
(code, err.as_str()),
(1, "oldcode:-1 code:511\nuncompress: corrupt input\n")
);
assert!(d.join("w.Z").exists() && !d.join("w").exists());
}
#[cfg(unix)]
#[test]
fn an_unreadable_directory_is_reported_on_standard_output() {
use std::os::unix::fs::PermissionsExt;
let d = scratch("unreadable");
std::fs::create_dir_all(d.join("dir/sub")).unwrap();
std::fs::write(d.join("dir/a"), text(1000)).unwrap();
let sub = d.join("dir/sub");
std::fs::set_permissions(&sub, std::fs::Permissions::from_mode(0o000)).unwrap();
let (code, out, err) = run(&d, "compress", &["-r", "dir"], b"");
assert_eq!((code, err.as_str()), (0, ""));
assert_eq!(out, b"dir/sub unreadable\n");
assert!(d.join("dir/a.Z").exists());
}
#[test]
fn hostile_streams_never_panic() {
let d = scratch("hostile");
let data = text(20_000);
let (_, good, _) = run(&d, "compress", &["-b", "12"], &data);
let mut x = 0x2545_f491_4f6c_dd1du64;
let mut next = move || {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
x
};
for round in 0..300 {
let mut b = good.clone();
match round % 3 {
0 => {
for _ in 0..1 + next() % 4 {
let at = (next() as usize) % b.len();
b[at] = next() as u8;
}
}
1 => b.truncate((next() as usize) % b.len()),
_ => {
b.truncate(3 + (next() as usize) % 64);
for v in &mut b[3..] {
*v = next() as u8;
}
b[2] = next() as u8;
}
}
let (code, _, _) = run(&d, "uncompress", &["-c"], &b);
assert!(matches!(code, 0 | 1 | 2 | 4), "round {round}: exit {code}");
}
}
}