#[cfg(unix)]
mod compat_harness;
#[cfg(unix)]
mod unix {
use super::compat_harness::*;
fn kib() -> Vec<u8> {
(0..1024u32).map(|i| (i * 7 % 251) as u8).collect()
}
fn case<'a>(args: Vec<&'a str>) -> Case<'a> {
Case {
name: "bzip2",
args,
files: vec![("a.txt", kib())],
stdin: None,
tty_stdin: false,
tty_stdout: false,
env: vec![],
compare_output_bytes: true,
expect_stdout_plain: None,
setup: None,
roundtrip: Roundtrip::Bzip2,
}
}
#[test]
fn reference_against_itself_is_clean() {
let c = case(vec!["a.txt"]);
let Some(a) = reference(REFERENCE_DIR, &c) else {
return;
};
let b = reference(REFERENCE_DIR, &c).unwrap();
assert_eq!(a.code, 0);
assert!(a.tree.contains_key("a.bz2") || a.tree.contains_key("a.txt.bz2"));
assert_same(&c, &a, &b);
}
#[test]
fn a_missing_reference_fails_only_on_ci() {
let missing = std::path::Path::new("/nonexistent/stuffr-no-such-tool");
let r = std::panic::catch_unwind(|| require_reference(missing));
if std::env::var_os("CI").is_some() {
assert!(r.is_err(), "CI is set, so a missing reference must panic");
} else {
assert_eq!(r.ok(), Some(false));
}
assert!(require_reference(std::path::Path::new("/bin/sh")));
}
#[test]
fn tty_stdout_is_a_terminal_for_the_reference() {
let mut c = case(vec!["-c", "a.txt"]);
c.tty_stdout = true;
let Some(r) = reference(REFERENCE_DIR, &c) else {
return;
};
assert_eq!(r.code, 1, "stderr: {}", r.stderr);
assert!(
r.stderr
.contains("I won't write compressed data to a terminal"),
"{}",
r.stderr
);
}
#[test]
fn selftest_bzip2_c_one_kib() {
let c = case(vec!["-c", "a.txt"]);
let Some(r) = reference(REFERENCE_DIR, &c) else {
return;
};
let s = stuffr_as(&c);
assert_same(&c, &r, &s);
}
#[test]
fn tty_stdin_is_fed_and_closed() {
for data in [&b"hello world\n"[..], &b"no trailing newline"[..]] {
let mut c = case(vec!["-c"]);
c.files.clear();
c.stdin = Some(data.to_vec());
c.tty_stdin = true;
let Some(a) = reference(REFERENCE_DIR, &c) else {
return;
};
let b = reference(REFERENCE_DIR, &c).unwrap();
assert_eq!(a.code, 0, "stderr: {}", a.stderr);
assert!(a.stdout.starts_with(b"BZh"));
let mut want = data.to_vec();
if !want.ends_with(b"\n") {
want.push(b'\n');
}
assert_eq!(ref_decompress(&a.stdout).unwrap(), want);
assert_same(&c, &a, &b);
}
}
#[test]
fn tty_stdout_larger_than_the_pty_buffer() {
let big: Vec<u8> = (0..1_048_576u32)
.map(|i| b'a' + (i.wrapping_mul(2654435761) >> 28) as u8)
.collect();
let mut mk = case(vec!["-c"]);
mk.files.clear();
mk.stdin = Some(big.clone());
let Some(packed) = reference(REFERENCE_DIR, &mk) else {
return;
};
let mut c = case(vec!["big.bz2"]);
c.name = "bzcat";
c.files = vec![("big.bz2", packed.stdout)];
c.tty_stdout = true;
let Some(a) = reference(REFERENCE_DIR, &c) else {
return;
};
let b = reference(REFERENCE_DIR, &c).unwrap();
assert_eq!(a.code, 0, "stderr: {}", a.stderr);
assert!(a.stdout.len() >= 1_048_576, "{}", a.stdout.len());
assert_same(&c, &a, &b);
}
fn base_run() -> Run {
let mut tree = Tree::new();
tree.insert("a.txt".into(), (kib(), 0o644, INPUT_MTIME as i64));
tree.insert("a.txt.bz2".into(), (b"BZh9xyz".to_vec(), 0o644, now_secs()));
Run {
code: 0,
stdout: b"out".to_vec(),
stderr: "bzip2: msg\n".into(),
tree,
start: now_secs(),
}
}
fn verdict(c: &Case, r: &Run, s: &Run) -> Option<String> {
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| assert_same(c, r, s)));
res.err().map(|p| {
p.downcast_ref::<String>()
.cloned()
.or_else(|| p.downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_default()
})
}
fn mutated(c: &Case, mutate: impl FnOnce(&mut Run)) -> Option<String> {
let r = base_run();
let mut s = base_run();
mutate(&mut s);
verdict(c, &r, &s)
}
fn notices(c: &Case, mutate: impl FnOnce(&mut Run), needle: &str) {
let msg = mutated(c, mutate).expect("assert_same did not notice the difference");
assert!(msg.contains(needle), "message lacks {needle:?}:\n{msg}");
assert!(
msg.contains("in 1 way(s)"),
"more than one difference:\n{msg}"
);
}
#[test]
fn identical_runs_pass() {
assert_eq!(mutated(&case(vec![]), |_| {}), None);
}
#[test]
fn notices_mode() {
notices(
&case(vec![]),
|s| s.tree.get_mut("a.txt.bz2").unwrap().1 = 0o600,
"mode reference",
);
}
#[test]
fn notices_pinned_input_mtime() {
notices(
&case(vec![]),
|s| s.tree.get_mut("a.txt").unwrap().2 += 1,
"a.txt mtime",
);
}
#[test]
fn mtime_window_accepts_two_wall_clock_times() {
let now = now_secs();
let r = base_run();
let mut s = base_run();
s.tree.get_mut("a.txt.bz2").unwrap().2 = now - 1;
assert_eq!(verdict(&case(vec![]), &r, &s), None);
}
#[test]
fn mtime_window_rejects_a_time_far_outside() {
notices(
&case(vec![]),
|s| s.tree.get_mut("a.txt.bz2").unwrap().2 = now_secs() - 1000,
"a.txt.bz2 mtime",
);
}
#[test]
fn notices_removed_file() {
notices(
&case(vec![]),
|s| {
s.tree.remove("a.txt");
},
"removed in stuffr",
);
}
#[test]
fn notices_added_file() {
notices(
&case(vec![]),
|s| {
s.tree.insert("extra".into(), (vec![], 0o644, now_secs()));
},
"added in stuffr",
);
}
#[test]
fn notices_stderr_text() {
notices(
&case(vec![]),
|s| s.stderr = "bzip2: other\n".into(),
"stderr differs",
);
}
#[test]
fn notices_stdout_bytes() {
notices(
&case(vec![]),
|s| s.stdout = b"oux".to_vec(),
"stdout differs",
);
}
#[test]
fn notices_output_bytes_when_identity_is_promised() {
notices(
&case(vec![]),
|s| s.tree.get_mut("a.txt.bz2").unwrap().0 = b"BZh9xyy".to_vec(),
"a.txt.bz2 bytes differ",
);
}
#[test]
fn notices_exit_code() {
notices(&case(vec![]), |s| s.code = 3, "exit code");
}
#[test]
fn notices_block_digit_in_roundtrip_mode() {
let mut c9 = case(vec!["-c", "-9", "a.txt"]);
c9.compare_output_bytes = false;
let Some(r) = reference(REFERENCE_DIR, &c9) else {
return;
};
let c1 = case(vec!["-c", "-1", "a.txt"]);
let s = reference(REFERENCE_DIR, &c1).unwrap();
assert_eq!(r.stdout[3], b'9');
assert_eq!(s.stdout[3], b'1');
let msg = verdict(&c9, &r, &s).expect("block digit difference not noticed");
assert!(msg.contains("block-size digit"), "{msg}");
assert!(msg.contains("in 1 way(s)"), "{msg}");
}
#[test]
fn program_path_is_normalised_to_the_bare_name() {
let exe = std::path::Path::new("/tmp/x/bzip2");
let norm = |raw: &str| normalise_stderr(raw.as_bytes(), exe, "bzip2");
assert_eq!(norm("/tmp/x/bzip2: oops\n"), "bzip2: oops\n");
assert_eq!(
norm("see /tmp/x/bzip2 and /tmp/x/bzip2\n"),
"see bzip2 and bzip2\n"
);
assert_eq!(norm("/tmp/y/bzip2: oops\n"), "/tmp/y/bzip2: oops\n");
assert_eq!(
norm("/tmp/x/bzip2.bak: oops /tmp/x/bzip2/sub\n"),
"/tmp/x/bzip2.bak: oops /tmp/x/bzip2/sub\n"
);
assert_eq!(norm("at end /tmp/x/bzip2"), "at end bzip2");
assert_eq!(norm("/tmp/x/bzip2\tx"), "bzip2\tx");
}
#[test]
fn a_different_message_still_differs_after_normalisation() {
let exe = std::path::Path::new("/tmp/x/bzip2");
let r = base_run();
let mut s = base_run();
s.stderr = normalise_stderr(b"/tmp/x/bzip2: different\n", exe, "bzip2");
let msg = verdict(&case(vec![]), &r, &s).expect("not noticed");
assert!(msg.contains("stderr differs"), "{msg}");
s.stderr = normalise_stderr(b"/tmp/x/bzip2: msg\n", exe, "bzip2");
assert_eq!(verdict(&case(vec![]), &r, &s), None);
}
}
#[cfg(unix)]
mod differential {
use super::compat_harness::*;
use std::path::Path;
use std::process::{Command, Stdio};
struct XorShift(u64);
impl XorShift {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
}
fn random(n: usize, seed: u64) -> Vec<u8> {
let mut r = XorShift(seed);
(0..n).map(|_| (r.next() >> 24) as u8).collect()
}
const WORDS: &[&str] = &[
"the",
"quick",
"brown",
"fox",
"jumps",
"over",
"lazy",
"dog",
"stuffr",
"bzip2",
"block",
"sorting",
"compressor",
"and",
"of",
"a",
"to",
"in",
"file",
];
fn text(n: usize, seed: u64) -> Vec<u8> {
let mut r = XorShift(seed);
let mut v = Vec::with_capacity(n + 16);
while v.len() < n {
v.extend_from_slice(WORDS[(r.next() % WORDS.len() as u64) as usize].as_bytes());
v.push(if r.next().is_multiple_of(9) {
b'\n'
} else {
b' '
});
}
v.truncate(n);
v
}
fn mixed(n: usize, seed: u64) -> Vec<u8> {
let mut r = XorShift(seed);
let mut v = Vec::with_capacity(n + 4096);
while v.len() < n {
match r.next() % 3 {
0 => v.extend(text(2048, r.next())),
1 => v.extend(random(512, r.next() | 1)),
_ => v.extend(std::iter::repeat_n(b'z', (r.next() % 3000) as usize)),
}
}
v.truncate(n);
v
}
fn repetitive(n: usize) -> Vec<u8> {
b"abcabcabd".iter().copied().cycle().take(n).collect()
}
fn kib_text() -> Vec<u8> {
text(1024, 7)
}
fn ref_bz(data: &[u8], level: &str) -> Option<Vec<u8>> {
let exe = Path::new(REFERENCE_DIR).join("bzip2");
if !require_reference(&exe) {
return None;
}
let mut child = Command::new(exe)
.args(["-c", level])
.env_clear()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
let mut si = child.stdin.take().unwrap();
let d = data.to_vec();
let w = std::thread::spawn(move || {
use std::io::Write;
let _ = si.write_all(&d);
});
let out = child.wait_with_output().unwrap();
w.join().unwrap();
assert!(out.status.success());
Some(out.stdout)
}
fn case<'a>(name: &'a str, args: &[&'a str], files: Vec<(&'a str, Vec<u8>)>) -> Case<'a> {
Case {
name,
args: args.to_vec(),
files,
stdin: None,
tty_stdin: false,
tty_stdout: false,
env: vec![],
compare_output_bytes: true,
expect_stdout_plain: None,
setup: None,
roundtrip: Roundtrip::Bzip2,
}
}
fn panic_text(p: Box<dyn std::any::Any + Send>) -> String {
p.downcast_ref::<String>()
.cloned()
.or_else(|| p.downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_default()
}
fn check_all_with(group: &str, cases: &[Case], adjust: fn(&mut Run, &mut Run)) {
let one = |c: &Case| -> Result<bool, String> {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let Some(mut r) = reference(REFERENCE_DIR, c) else {
return false;
};
let mut s = stuffr_as(c);
adjust(&mut r, &mut s);
assert_same(c, &r, &s);
true
}))
.map_err(panic_text)
};
let workers = std::thread::available_parallelism().map_or(2, |n| n.get().min(8));
let next = std::sync::atomic::AtomicUsize::new(0);
let results: Vec<(usize, Result<bool, String>)> = std::thread::scope(|sc| {
let hs: Vec<_> = (0..workers)
.map(|_| {
sc.spawn(|| {
let mut out = Vec::new();
loop {
let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let Some(c) = cases.get(i) else { break out };
out.push((i, one(c)));
}
})
})
.collect();
hs.into_iter().flat_map(|h| h.join().unwrap()).collect()
});
let mut results = results;
results.sort_by_key(|(i, _)| *i);
let mut failures = Vec::new();
let mut ran = 0;
for (_, r) in results {
match r {
Ok(true) => ran += 1,
Ok(false) => {}
Err(p) => failures.push(p),
}
}
println!("{group}: {ran} of {} cases compared", cases.len());
if std::env::var_os("CI").is_some() {
assert_eq!(
ran,
cases.len(),
"{group}: CI is set but only {ran} of {} cases were compared \
(is the reference bzip2 installed?)",
cases.len()
);
}
assert!(
failures.is_empty(),
"{group}: {} of {} cases differ:\n\n{}",
failures.len(),
cases.len(),
failures.join("\n\n")
);
}
fn check_all(group: &str, cases: &[Case]) {
check_all_with(group, cases, |_, _| {});
}
fn link_l_txt(d: &Path) {
std::os::unix::fs::symlink("a.txt", d.join("l.txt")).unwrap();
}
fn link_l_bz2(d: &Path) {
std::os::unix::fs::symlink("a.bz2", d.join("l.bz2")).unwrap();
}
fn dangling_output(d: &Path) {
std::os::unix::fs::symlink("nowhere", d.join("a.txt.bz2")).unwrap();
}
fn hard_link(d: &Path) {
std::fs::hard_link(d.join("a.txt"), d.join("h.txt")).unwrap();
}
fn two_hard_links(d: &Path) {
std::fs::hard_link(d.join("a.txt"), d.join("h.txt")).unwrap();
std::fs::hard_link(d.join("a.txt"), d.join("i.txt")).unwrap();
}
fn mode(d: &Path, name: &str, m: u32) {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(d.join(name), std::fs::Permissions::from_mode(m)).unwrap();
}
fn unreadable(d: &Path) {
mode(d, "a.txt", 0o000);
}
fn mode_640(d: &Path) {
mode(d, "a.txt", 0o640);
}
fn mode_604_bz2(d: &Path) {
mode(d, "a.bz2", 0o604);
}
fn subdir(d: &Path) {
std::fs::create_dir(d.join("d")).unwrap();
}
fn empty_dir_output(d: &Path) {
std::fs::create_dir(d.join("a.txt.bz2")).unwrap();
}
#[test]
fn flags() {
let t = kib_text();
let Some(bz) = ref_bz(&t, "-9") else { return };
let a = || vec![("a.txt", t.clone())];
let b = || vec![("a.bz2", bz.clone())];
let ab = || vec![("a.txt", t.clone()), ("a.bz2", bz.clone())];
let mut cases = vec![
case("bzip2", &["a.txt"], a()),
case("bzip2", &["-k", "a.txt"], a()),
case("bzip2", &["--keep", "a.txt"], a()),
case("bzip2", &["-c", "a.txt"], a()),
case("bzip2", &["--stdout", "a.txt"], a()),
case("bzip2", &["-z", "a.txt"], a()),
case("bzip2", &["--compress", "a.txt"], a()),
case("bzip2", &["-f", "a.txt"], a()),
case("bzip2", &["--force", "a.txt"], a()),
case("bzip2", &["-q", "a.txt"], a()),
case("bzip2", &["--quiet", "a.txt"], a()),
case("bzip2", &["-v", "a.txt"], a()),
case("bzip2", &["--verbose", "a.txt"], a()),
case("bzip2", &["-s", "a.txt"], a()),
case("bzip2", &["--small", "a.txt"], a()),
case("bzip2", &["-s9", "a.txt"], a()),
case("bzip2", &["-s1", "a.txt"], a()),
case("bzip2", &["--fast", "a.txt"], a()),
case("bzip2", &["--best", "a.txt"], a()),
case("bzip2", &["--exponential", "a.txt"], a()),
case("bzip2", &["-zc9", "a.txt"], a()),
case("bzip2", &["-kv", "a.txt"], a()),
case("bzip2", &["-v", "-4", "a.txt"], a()),
case("bzip2", &["a.txt", "-k"], a()),
case("bzip2", &["-d", "-z", "a.txt"], a()),
case("bzip2", &["-", "a.txt"], a()),
case("bzip2", &["--", "a.txt"], a()),
case("bzip2", &["-k", "--", "-c"], vec![("-c", t.clone())]),
case("bzip2", &["--repetitive-fast", "a.txt"], a()),
case("bzip2", &["--repetitive-best", "a.txt"], a()),
case("bzip2", &["-x"], a()),
case("bzip2", &["-kx", "a.txt"], a()),
case("bzip2", &["--bogus", "a.txt"], a()),
case("bzip2", &["--bogus", "-x"], a()),
case("bzip2", &["-h"], a()),
case("bzip2", &["--help"], a()),
case("bzip2", &["-x", "-h"], a()),
case("bzip2", &["-h", "-x"], a()),
case("bzip2", &["--help", "-x"], a()),
case("bzip2", &["-c", "-t", "a.txt"], a()),
case("bzip2", &["-ct"], a()),
case("bzip2", &["-d", "a.bz2"], b()),
case("bzip2", &["--decompress", "a.bz2"], b()),
case("bzip2", &["-dkv", "a.bz2"], b()),
case("bzip2", &["-dc", "a.bz2"], b()),
case("bzip2", &["-ds", "a.bz2"], b()),
case("bzip2", &["-t", "a.bz2"], b()),
case("bzip2", &["--test", "a.bz2"], b()),
case("bzip2", &["-tv", "a.bz2"], b()),
case("bzip2", &["-tq", "a.bz2"], b()),
case("bzip2", &["-tdv", "a.bz2"], b()),
case("bzip2", &["-td", "a.bz2"], b()),
case("bzip2", &["-v", "a.txt", "-d", "a.bz2"], ab()),
case("bunzip2", &["a.bz2"], b()),
case("bunzip2", &["-k", "a.bz2"], b()),
case("bunzip2", &["-z", "a.txt"], a()),
case("bunzip2", &["-t", "a.bz2"], b()),
case("bunzip2", &["-c", "a.bz2"], b()),
case("bzcat", &["a.bz2"], b()),
case("bzcat", &["-z", "a.txt"], a()),
case("bzcat", &["-t", "a.bz2"], b()),
case("bzcat", &["-v", "a.bz2"], b()),
case("bzcat", &["-k", "a.bz2"], b()),
];
let mut env = |vars: Vec<(&'static str, &'static str)>, args: &[&'static str]| {
let mut c = case("bzip2", args, ab());
c.env = vars;
cases.push(c);
};
env(vec![("BZIP2", "-9 -v")], &["-1", "a.txt"]);
env(vec![("BZIP2", "-1 -v")], &["a.txt"]);
env(vec![("BZIP", "-k")], &["a.txt"]);
env(vec![("BZIP2", "-c"), ("BZIP", "-1")], &["a.txt"]);
env(vec![("BZIP2", " -k\t-v ")], &["a.txt"]);
env(vec![("BZIP2", "a.txt")], &["-k"]);
env(vec![("BZIP2", "-x")], &["a.txt"]);
env(vec![("BZIP", "-d")], &["a.bz2"]);
env(vec![("BZIP2", "--")], &["-k", "a.txt"]);
check_all("flags", &cases);
}
fn reference_exits_after_license() -> Option<bool> {
let exe = Path::new(REFERENCE_DIR).join("bzip2");
if !require_reference(&exe) {
return None;
}
let out = Command::new(exe)
.arg("-V")
.env_clear()
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()
.unwrap();
Some(out.stdout.is_empty())
}
const REF_LICENSE_FIRST: &str =
"bzip2, a block-sorting file compressor. Version 1.0.8, 13-Jul-2019.\n";
fn licence_line(r: &mut Run, s: &mut Run) {
let first = format!(
"bzip2 (stuffr {}), a block-sorting file compressor. \
Version 1.0.8, 13-Jul-2019.\n",
env!("CARGO_PKG_VERSION")
);
assert!(
r.stderr.contains(REF_LICENSE_FIRST),
"reference has no licence line"
);
assert!(
s.stderr.contains(&first),
"stuffr's licence line is not {first:?}: {:?}",
s.stderr
);
assert!(!s.stderr.contains(&format!("{REF_LICENSE_FIRST} \n")));
s.stderr = s.stderr.replace(&first, REF_LICENSE_FIRST);
}
fn licence_text_only(r: &mut Run, s: &mut Run) {
licence_line(r, s);
s.code = r.code;
s.stdout = r.stdout.clone();
s.tree = r.tree.clone();
}
#[test]
fn license_and_version() {
let Some(legacy) = reference_exits_after_license() else {
return;
};
let t = kib_text();
let bz = ref_bz(b"x", "-9").unwrap();
let a = || vec![("a.txt", t.clone())];
let fed = |name: &'static str, args: &[&'static str]| {
let mut c = case(name, args, vec![]);
c.stdin = Some(bz.clone());
c
};
let alone = vec![
case("bzip2", &["-L"], vec![]),
case("bzip2", &["-V"], vec![]),
case("bzip2", &["--license"], vec![]),
case("bzip2", &["--version"], vec![]),
];
if legacy {
println!(
"license: the reference exits after the licence (Debian's \
20-legacy.patch); skipping the continuation cases and \
comparing the licence text only"
);
check_all_with("license", &alone, licence_text_only);
return;
}
let mut cases = alone;
cases.extend([
case("bzip2", &["-LV"], vec![]),
case("bzip2", &["-V", "-k", "a.txt"], a()),
case("bzip2", &["-V", "-x"], vec![]),
fed("bunzip2", &["-L"]),
fed("bzcat", &["--version"]),
]);
check_all_with("license", &cases, licence_line);
}
#[test]
fn compress() {
let inputs: Vec<(&str, Vec<u8>)> = vec![
("empty", vec![]),
("one", b"x".to_vec()),
("text", kib_text()),
("mixed", mixed(1 << 20, 11)),
("random", random(1 << 20, 12)),
("repetitive", repetitive(4 << 20)),
];
let levels = ["", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9"];
let mut cases = Vec::new();
for (name, data) in &inputs {
for l in levels {
let mut args = vec!["-v"];
if !l.is_empty() {
args.push(l);
}
args.push(name);
cases.push(case("bzip2", &args, vec![(name, data.clone())]));
}
}
for (_, data) in &inputs[..4] {
let mut c = case("bzip2", &["-v"], vec![]);
c.stdin = Some(data.clone());
cases.push(c);
}
check_all("compress", &cases);
}
#[test]
fn decompress() {
let t = kib_text();
let big = mixed(300_000, 21);
let Some(bz) = ref_bz(&t, "-9") else { return };
let bz_big = ref_bz(&big, "-1").unwrap();
let bz2nd = ref_bz(b"second stream\n", "-3").unwrap();
let cat2 = [bz.clone(), bz2nd.clone()].concat();
let with = |tail: &[u8]| [bz.clone(), tail.to_vec()].concat();
let mut damaged = bz_big.clone();
let mid = damaged.len() / 2;
damaged[mid] ^= 0x55;
let mut bad_crc = bz.clone();
let n = bad_crc.len();
bad_crc[n - 2] ^= 0x01;
let mut cases = Vec::new();
for name in ["a.bz2", "a.bz", "a.tbz2", "a.tbz", "a.dat", "a"] {
cases.push(case("bunzip2", &[name], vec![(name, bz.clone())]));
cases.push(case("bunzip2", &["-v", name], vec![(name, bz.clone())]));
}
cases.push(case("bunzip2", &[".bz2"], vec![(".bz2", bz.clone())]));
let files: Vec<(&str, Vec<u8>, &str)> = vec![
("cat2.bz2", cat2.clone(), "concatenated"),
("big.bz2", bz_big.clone(), "multi-block"),
("zeros.bz2", with(&[0u8; 40]), "trailing zeros"),
("garb.bz2", with(b"garbage"), "trailing text"),
("digit.bz2", with(b"BZhX..."), "trailing bad digit"),
(
"blockmagic.bz2",
with(b"BZh9xxxxxxxxxx"),
"trailing bad block",
),
("damaged.bz2", damaged.clone(), "damaged"),
("crc.bz2", bad_crc.clone(), "stream CRC"),
("plain.bz2", t.clone(), "not bzip2"),
];
for (name, data, _) in &files {
let name: &str = name;
for args in [
vec![name],
vec!["-k", name],
vec!["-v", name],
vec!["-q", name],
vec!["-f", name],
vec!["-c", name],
vec!["-cf", name],
vec!["-t", name],
vec!["-tv", name],
vec!["-tq", name],
] {
let mut c = case("bunzip2", &args, vec![(name, data.clone())]);
c.compare_output_bytes = true;
cases.push(c);
}
let mut c = case("bunzip2", &[], vec![]);
c.stdin = Some(data.clone());
cases.push(c);
let mut c = case("bunzip2", &["-f"], vec![]);
c.stdin = Some(data.clone());
cases.push(c);
let mut c = case("bzip2", &["-t"], vec![]);
c.stdin = Some(data.clone());
cases.push(c);
}
cases.push(case(
"bunzip2",
&["damaged.bz2", "a.bz2"],
vec![("damaged.bz2", damaged.clone()), ("a.bz2", bz.clone())],
));
cases.push(case(
"bunzip2",
&["-q", "damaged.bz2", "a.bz2"],
vec![("damaged.bz2", damaged.clone()), ("a.bz2", bz.clone())],
));
cases.push(case(
"bunzip2",
&["plain.bz2", "a.bz2"],
vec![("plain.bz2", t.clone()), ("a.bz2", bz.clone())],
));
cases.push(case(
"bzip2",
&["-t", "plain.bz2", "damaged.bz2", "a.bz2"],
vec![
("plain.bz2", t.clone()),
("damaged.bz2", damaged.clone()),
("a.bz2", bz.clone()),
],
));
check_all("decompress", &cases);
}
fn blank_perror(stderr: &str) -> String {
const AFTER: &str = "*Possible* reason follows.\n";
let mut out = String::with_capacity(stderr.len());
let mut rest = stderr;
while let Some(i) = rest.find(AFTER) {
let (head, tail) = rest.split_at(i + AFTER.len());
out.push_str(head);
let end = tail.find('\n').map_or(tail.len(), |n| n + 1);
out.push_str("<perror>\n");
rest = &tail[end..];
}
out.push_str(rest);
out
}
fn perror_blanked(r: &mut Run, s: &mut Run) {
r.stderr = blank_perror(&r.stderr);
s.stderr = blank_perror(&s.stderr);
}
#[test]
fn blank_perror_replaces_only_that_line() {
let text = "\nbunzip2: Compressed file ends unexpectedly;\n\tperhaps it is \
corrupted? *Possible* reason follows.\nbunzip2: Success\n\tInput \
file = a, output file = b\n";
assert_eq!(
blank_perror(text),
"\nbunzip2: Compressed file ends unexpectedly;\n\tperhaps it is \
corrupted? *Possible* reason follows.\n<perror>\n\tInput file = a, \
output file = b\n"
);
assert_eq!(blank_perror("no such line\n"), "no such line\n");
}
#[test]
fn decompress_truncated() {
let big = mixed(300_000, 22);
let Some(bz) = ref_bz(&big, "-2") else { return };
let t = kib_text();
let bzt = ref_bz(&t, "-9").unwrap();
let mut cases = Vec::new();
let files: Vec<(&str, Vec<u8>)> = vec![
("trunc.bz2", bz[..bz.len() / 2].to_vec()),
("short.bz2", bz[..20].to_vec()),
("empty.bz2", vec![]),
("tailb.bz2", [bzt.clone(), b"B".to_vec()].concat()),
("tailbzh.bz2", [bzt.clone(), b"BZh".to_vec()].concat()),
("bz.bz2", b"BZ".to_vec()),
];
for (name, data) in &files {
let name: &str = name;
for args in [
vec![name],
vec!["-v", name],
vec!["-q", name],
vec!["-c", name],
vec!["-t", name],
vec!["-tv", name],
] {
cases.push(case("bunzip2", &args, vec![(name, data.clone())]));
}
let mut c = case("bunzip2", &[], vec![]);
c.stdin = Some(data.clone());
cases.push(c);
cases.push(case(
"bunzip2",
&[name, "a.bz2"],
vec![(name, data.clone()), ("a.bz2", bzt.clone())],
));
}
cases.push(case("bunzip2", &["-c", "/dev/null"], vec![]));
cases.push(case("bunzip2", &[], vec![]));
if cfg!(target_os = "macos") {
check_all("decompress_truncated", &cases);
} else {
check_all_with("decompress_truncated", &cases, perror_blanked);
}
}
#[test]
fn files() {
let t = kib_text();
let Some(bz) = ref_bz(&t, "-9") else { return };
let a = || vec![("a.txt", t.clone())];
let with_setup = |mut c: Case<'static>, f: fn(&Path)| {
c.setup = Some(f);
c
};
let long: &'static str = Box::leak("x".repeat(1100).into_boxed_str());
let long_ok: &'static str = Box::leak("y".repeat(1024).into_boxed_str());
let cases = vec![
case(
"bzip2",
&["a.txt"],
vec![("a.txt", t.clone()), ("a.txt.bz2", b"old".to_vec())],
),
case(
"bzip2",
&["-f", "a.txt"],
vec![("a.txt", t.clone()), ("a.txt.bz2", b"old".to_vec())],
),
case(
"bunzip2",
&["a.bz2"],
vec![("a.bz2", bz.clone()), ("a", b"old".to_vec())],
),
case(
"bunzip2",
&["-f", "a.bz2"],
vec![("a.bz2", bz.clone()), ("a", b"old".to_vec())],
),
case(
"bunzip2",
&["-fk", "a.bz2"],
vec![("a.bz2", bz.clone()), ("a", b"old".to_vec())],
),
with_setup(case("bzip2", &["a.txt"], a()), dangling_output),
with_setup(case("bzip2", &["-f", "a.txt"], a()), dangling_output),
with_setup(case("bzip2", &["a.txt"], a()), empty_dir_output),
with_setup(case("bzip2", &["-f", "a.txt"], a()), empty_dir_output),
case("bzip2", &["-k", "a.txt"], a()),
case("bzip2", &["a.txt"], a()),
case("bunzip2", &["-k", "a.bz2"], vec![("a.bz2", bz.clone())]),
with_setup(case("bzip2", &["l.txt"], a()), link_l_txt),
with_setup(case("bzip2", &["-f", "l.txt"], a()), link_l_txt),
with_setup(case("bzip2", &["-c", "l.txt"], a()), link_l_txt),
with_setup(
case("bunzip2", &["l.bz2"], vec![("a.bz2", bz.clone())]),
link_l_bz2,
),
with_setup(
case("bunzip2", &["-f", "l.bz2"], vec![("a.bz2", bz.clone())]),
link_l_bz2,
),
with_setup(
case("bzip2", &["-t", "l.bz2"], vec![("a.bz2", bz.clone())]),
link_l_bz2,
),
case("bzip2", &["/dev/null"], vec![]),
case("bzip2", &["-f", "/dev/null"], vec![]),
case("bzip2", &["-c", "/dev/null"], vec![]),
case("bzip2", &["-t", "/dev/null"], vec![]),
with_setup(case("bzip2", &["d"], vec![]), subdir),
with_setup(case("bunzip2", &["d"], vec![]), subdir),
with_setup(case("bzip2", &["-t", "d"], vec![]), subdir),
with_setup(case("bzip2", &["-c", "d"], vec![]), subdir),
with_setup(case("bzip2", &["a.txt"], a()), unreadable),
with_setup(case("bunzip2", &["a.txt"], a()), unreadable),
with_setup(case("bzip2", &["-t", "a.txt"], a()), unreadable),
with_setup(case("bzip2", &["-c", "a.txt"], a()), unreadable),
case("bzip2", &["missing"], vec![]),
case("bunzip2", &["missing.bz2"], vec![]),
case("bzcat", &["missing.bz2"], vec![]),
case("bzip2", &["-t", "missing.bz2"], vec![]),
case("bzip2", &[long], vec![]),
case("bzip2", &[long_ok], vec![]),
case("bzip2", &["missing", "a.txt"], a()),
case(
"bzip2",
&["a.txt", "missing", "b.txt"],
vec![("a.txt", t.clone()), ("b.txt", b"b".to_vec())],
),
case(
"bunzip2",
&["a.bz2", "missing.bz2", "plain.bz2"],
vec![("a.bz2", bz.clone()), ("plain.bz2", t.clone())],
),
case(
"bzip2",
&["-v", "a.txt", "much-longer-name.txt"],
vec![("a.txt", t.clone()), ("much-longer-name.txt", t.clone())],
),
case("bzip2", &["a.bz2"], vec![("a.bz2", bz.clone())]),
case("bzip2", &["-q", "a.bz"], vec![("a.bz", bz.clone())]),
case("bzip2", &["-c", "a.tbz2"], vec![("a.tbz2", bz.clone())]),
case(
"bzip2",
&["a.tbz", "a.txt"],
vec![("a.tbz", bz.clone()), ("a.txt", t.clone())],
),
with_setup(case("bzip2", &["a.txt"], a()), hard_link),
with_setup(case("bzip2", &["a.txt"], a()), two_hard_links),
with_setup(case("bzip2", &["-f", "a.txt"], a()), hard_link),
with_setup(case("bzip2", &["-c", "a.txt"], a()), hard_link),
with_setup(case("bzip2", &["a.txt"], a()), mode_640),
with_setup(case("bzip2", &["-k", "a.txt"], a()), mode_640),
with_setup(
case("bunzip2", &["a.bz2"], vec![("a.bz2", bz.clone())]),
mode_604_bz2,
),
with_setup(
case(
"bunzip2",
&["a.bz2"],
vec![("a.bz2", [bz.clone(), b"junk".to_vec()].concat())],
),
mode_604_bz2,
),
case("bzip2", &["sub/a.txt"], vec![("sub/a.txt", t.clone())]),
];
let root = unsafe { libc::geteuid() } == 0;
let cases: Vec<_> = cases
.into_iter()
.filter(|c| {
let destructive = c.name == "bzip2" && c.args == ["-f", "/dev/null"];
if root && destructive {
eprintln!(
"files: skipping `bzip2 -f /dev/null` as root (it removes /dev/null)"
);
}
!(root && destructive)
})
.collect();
check_all("files", &cases);
}
#[test]
fn streams() {
let t = kib_text();
let Some(bz) = ref_bz(&t, "-9") else { return };
let bz2 = ref_bz(b"second\n", "-5").unwrap();
let piped = |name: &'static str, args: &[&'static str], data: Vec<u8>| {
let mut c = case(name, args, vec![]);
c.stdin = Some(data);
c
};
let tty_in = |name: &'static str, args: &[&'static str]| {
let mut c = case(name, args, vec![]);
c.stdin = Some(t.clone());
c.tty_stdin = true;
c
};
let tty_out = |name: &'static str, args: &[&'static str], files| {
let mut c = case(name, args, files);
c.tty_stdout = true;
c
};
let cases = vec![
piped("bzip2", &[], t.clone()),
piped("bzip2", &["-c"], t.clone()),
piped("bzip2", &["-3"], t.clone()),
piped("bunzip2", &[], bz.clone()),
piped("bzcat", &[], bz.clone()),
piped("bzip2", &["-d"], bz.clone()),
piped("bzip2", &["-dv"], bz.clone()),
piped("bzip2", &["-tv"], bz.clone()),
piped("bzip2", &["-d", "-"], bz.clone()),
case(
"bzcat",
&["a.bz2", "b.bz2"],
vec![("a.bz2", bz.clone()), ("b.bz2", bz2.clone())],
),
case(
"bzcat",
&["a.bz2", "plain", "b.bz2"],
vec![
("a.bz2", bz.clone()),
("plain", t.clone()),
("b.bz2", bz2.clone()),
],
),
case(
"bzip2",
&["-c", "a.txt", "b.txt"],
vec![("a.txt", t.clone()), ("b.txt", b"bee".to_vec())],
),
case(
"bzip2",
&["-dc", "a.bz2", "b.bz2"],
vec![("a.bz2", bz.clone()), ("b.bz2", bz2.clone())],
),
tty_out("bzip2", &[], vec![]),
tty_out("bzip2", &["-f"], vec![]),
tty_out("bzip2", &["-c", "a.txt"], vec![("a.txt", t.clone())]),
tty_out("bzip2", &["-cf", "a.txt"], vec![("a.txt", t.clone())]),
tty_out(
"bzip2",
&["-c", "a.txt", "a.bz2"],
vec![("a.txt", t.clone()), ("a.bz2", bz.clone())],
),
tty_out("bzcat", &["a.bz2"], vec![("a.bz2", bz.clone())]),
tty_out("bzip2", &["a.txt"], vec![("a.txt", t.clone())]),
tty_in("bunzip2", &[]),
tty_in("bunzip2", &["-f"]),
tty_in("bzcat", &[]),
tty_in("bzip2", &["-t"]),
tty_in("bzip2", &["-tf"]),
tty_in("bzip2", &["-c"]),
];
check_all("streams", &cases);
}
#[test]
fn verbosity_two_and_above_exit_code_and_files() {
let t = kib_text();
let Some(bz) = ref_bz(&t, "-9") else { return };
let cases = vec![
case("bzip2", &["-vv", "a.txt"], vec![("a.txt", t.clone())]),
case("bzip2", &["-vvvvv", "a.txt"], vec![("a.txt", t.clone())]),
case("bunzip2", &["-vv", "a.bz2"], vec![("a.bz2", bz.clone())]),
case("bzip2", &["-tvv", "a.bz2"], vec![("a.bz2", bz.clone())]),
];
check_all_with("verbosity", &cases, |r, s| s.stderr = r.stderr.clone());
}
#[test]
fn a_corrupt_input_leaves_no_output_and_keeps_the_input() {
let big = mixed(400_000, 31);
let Some(mut bz) = ref_bz(&big, "-1") else {
return;
};
let mid = bz.len() / 2;
bz[mid] ^= 0xff;
let c = case("bunzip2", &["c.bz2"], vec![("c.bz2", bz.clone())]);
let Some(r) = reference(REFERENCE_DIR, &c) else {
return;
};
let s = stuffr_as(&c);
assert_same(&c, &r, &s);
assert_eq!(s.code, 2);
assert_eq!(s.tree.len(), 1, "{:?}", s.tree.keys());
assert_eq!(s.tree["c.bz2"].0, bz);
}
fn closed_pipe_status(exe: &Path, dir: &Path) -> std::process::ExitStatus {
use std::io::Read;
let mut child = Command::new(exe)
.arg("big.bz2")
.current_dir(dir)
.env_clear()
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut out = child.stdout.take().unwrap();
let mut buf = [0u8; 4096];
let _ = out.read(&mut buf).unwrap();
drop(out);
let st = child.wait().unwrap();
let mut err = String::new();
let _ = child.stderr.take().unwrap().read_to_string(&mut err);
assert_eq!(err, "", "{exe:?} wrote to stderr");
st
}
#[test]
fn a_closed_stdout_pipe_ends_bzcat_by_sigpipe() {
use std::os::unix::process::ExitStatusExt;
let plain = text(4 << 20, 51);
let dir = std::env::temp_dir().join(format!("stuffr-sigpipe-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("bin")).unwrap();
let link = dir.join("bin").join("bzcat");
std::os::unix::fs::symlink(env!("CARGO_BIN_EXE_stuffr"), &link).unwrap();
let zip = dir.join("bin").join("bzip2");
std::os::unix::fs::symlink(env!("CARGO_BIN_EXE_stuffr"), &zip).unwrap();
std::fs::write(dir.join("big"), &plain).unwrap();
let st = Command::new(&zip)
.arg("big")
.current_dir(&dir)
.env_clear()
.status()
.unwrap();
assert!(st.success());
let ours = closed_pipe_status(&link, &dir);
assert_eq!(
ours.signal(),
Some(libc::SIGPIPE),
"stuffr as bzcat: {ours:?}"
);
let reference = Path::new(REFERENCE_DIR).join("bzcat");
if require_reference(&reference) {
let theirs = closed_pipe_status(&reference, &dir);
assert_eq!(
theirs.signal(),
Some(libc::SIGPIPE),
"reference: {theirs:?}"
);
} else {
println!("skipped the reference half: bzcat not installed");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn bzcat_streams_a_large_input() {
const N: usize = 64 << 20;
let plain = mixed(N, 41);
let Some(bz) = ref_bz(&plain, "-9") else {
return;
};
let peak = decoder_peak_rss("bzcat", "big.bz2", &bz, &plain, &[]);
assert!(
peak < 64 << 20,
"peak memory {peak} bytes is not under 64 MiB"
);
}
}
#[cfg(unix)]
mod interrupts {
use super::compat_harness::interrupt::*;
use super::compat_harness::{REFERENCE_DIR, ScratchDir, require_reference};
use std::cell::Cell;
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
const CAUGHT: &str = "\nbzip2: Control-C or similar caught, quitting.\n\
bzip2: Deleting output file a.bz2, if it exists.\n";
const CAUGHT_NO_OUTPUT: &str = "\nbzip2: Control-C or similar caught, quitting.\n";
const NOT_PROCESSED: &str = "bzip2: WARNING: some files have not been processed:\n\
bzip2: 2 specified on command line, 1 not processed yet.\n\n";
fn ours() -> &'static Path {
Path::new(env!("CARGO_BIN_EXE_stuffr"))
}
fn reference() -> Option<PathBuf> {
let exe = Path::new(REFERENCE_DIR).join("bzip2");
require_reference(&exe).then_some(exe)
}
fn scratch(tag: &str) -> ScratchDir {
ScratchDir::at(
std::env::temp_dir().join(format!("stuffr-bz-intr-{}-{tag}", std::process::id())),
)
}
fn plain(m: usize) -> Vec<u8> {
big_input(m).to_vec()
}
fn run(
mine: bool,
tag: &str,
args: &[&str],
inherit: Inherit,
sig: i32,
) -> Option<Interrupted> {
let dir = scratch(&format!("{tag}-{}", if mine { "ours" } else { "ref" }));
let exe = if mine {
ours().to_path_buf()
} else {
reference()?
};
let named = |n: &str| {
if mine {
n.starts_with(".stuffr-bzip2-")
} else {
n == "a.bz2"
}
};
let ready = has_data(&dir, &named);
interrupt_with_input(&exe, "bzip2", args, &dir, "a", &plain, inherit, sig, &ready)
}
fn check(r: &Interrupted, stderr: &str, left: &[&str], what: &str) {
assert_eq!(r.status.code(), Some(1), "{what}: {:?}", r.status);
assert_eq!(String::from_utf8_lossy(&r.stderr), stderr, "{what}");
assert_eq!(r.left, left, "{what}");
}
#[test]
fn bzip2_answers_int_term_and_hup() {
let _serial = interrupt_lock();
for sig in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP] {
for mine in [true, false] {
if let Some(r) = run(mine, &format!("s{sig}"), &["a"], Inherit::Default, sig) {
check(&r, CAUGHT, &["a"], &format!("mine={mine} signal {sig}"));
}
}
}
}
#[test]
fn bzip2_catches_int_even_when_ignored() {
let _serial = interrupt_lock();
for mine in [true, false] {
if let Some(r) = run(mine, "ign", &["a"], Inherit::IgnoreInt, libc::SIGINT) {
check(&r, CAUGHT, &["a"], &format!("mine={mine}"));
}
}
}
#[test]
fn bzip2_quiet_drops_the_deleting_line() {
let _serial = interrupt_lock();
for mine in [true, false] {
if let Some(r) = run(mine, "q", &["-q", "a"], Inherit::Default, libc::SIGINT) {
check(&r, CAUGHT_NO_OUTPUT, &["a"], &format!("mine={mine}"));
}
}
}
#[test]
fn bzip2_decompression_is_interrupted_too() {
let _serial = interrupt_lock();
for mine in [true, false] {
let dir = scratch(&format!("d-{}", if mine { "ours" } else { "ref" }));
let Some(exe) = (if mine {
Some(ours().to_path_buf())
} else {
reference()
}) else {
continue;
};
let named = |n: &str| {
if mine {
n.starts_with(".stuffr-bzip2-")
} else {
n == "a"
}
};
let ready = has_data(&dir, &named);
let Some(r) = interrupt_with_input(
&exe,
"bzip2",
&["-d", "a.bz2"],
&dir,
"a.bz2",
&zeros_bz2,
Inherit::Default,
libc::SIGTERM,
&ready,
) else {
continue;
};
assert_eq!(r.status.code(), Some(1), "mine={mine}: {:?}", r.status);
assert_eq!(
String::from_utf8_lossy(&r.stderr),
"\nbzip2: Control-C or similar caught, quitting.\n\
bzip2: Deleting output file a, if it exists.\n",
"mine={mine}"
);
assert_eq!(r.left, ["a.bz2"], "mine={mine}");
}
}
#[test]
fn bzip2_force_copy_of_a_non_bzip2_file_is_interrupted() {
let _serial = interrupt_lock();
for mine in [true, false] {
let dir = scratch(&format!("cat-{}", if mine { "ours" } else { "ref" }));
let Some(exe) = (if mine {
Some(ours().to_path_buf())
} else {
reference()
}) else {
continue;
};
let named = |n: &str| {
if mine {
n.starts_with(".stuffr-bzip2-")
} else {
n == "a"
}
};
let ready = has_data(&dir, &named);
let Some(r) = interrupt_with_input(
&exe,
"bzip2",
&["-df", "a.bz2"],
&dir,
"a.bz2",
&|m| plain(fast_size(m)),
Inherit::Default,
libc::SIGINT,
&ready,
) else {
continue;
};
check(
&r,
"\nbzip2: Control-C or similar caught, quitting.\n\
bzip2: Deleting output file a, if it exists.\n",
&["a.bz2"],
&format!("mine={mine}"),
);
}
}
#[test]
fn bzip2_counts_the_files_not_processed() {
let _serial = interrupt_lock();
for mine in [true, false] {
let dir = scratch(&format!("two-{}", if mine { "ours" } else { "ref" }));
let Some(exe) = (if mine {
Some(ours().to_path_buf())
} else {
reference()
}) else {
continue;
};
let named = |n: &str| {
if mine {
n.starts_with(".stuffr-bzip2-")
} else {
n == "a.bz2"
}
};
let mut got = None;
for mib in [64, 256] {
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
for f in ["a", "b"] {
std::fs::write(dir.join(f), big_input(mib)).unwrap();
}
let ready = has_data(&dir, &named);
got = interrupt(
&exe,
"bzip2",
&["a", "b"],
&dir,
Inherit::Default,
libc::SIGINT,
&ready,
);
if got.is_some() {
break;
}
}
if got.is_none() && take_reference_hung() {
eprintln!("note: bzip2 reference hung after the signal; comparison skipped");
continue;
}
let r = got.expect("finished before the signal even at 256 MiB");
check(
&r,
&format!("{CAUGHT}{NOT_PROCESSED}"),
&["a", "b"],
&format!("mine={mine}"),
);
}
}
#[test]
fn bzip2_test_mode_is_interrupted_without_deleting() {
let _serial = interrupt_lock();
for mine in [true, false] {
let dir = scratch(&format!("t-{}", if mine { "ours" } else { "ref" }));
let Some(exe) = (if mine {
Some(ours().to_path_buf())
} else {
reference()
}) else {
continue;
};
let first = Cell::new(None::<Instant>);
let ready = |_: &str| {
let t = first.get().unwrap_or_else(|| {
let now = Instant::now();
first.set(Some(now));
now
});
t.elapsed() >= Duration::from_millis(50)
};
let Some(r) = interrupt_with_input(
&exe,
"bzip2",
&["-t", "a.bz2"],
&dir,
"a.bz2",
&|mib| {
let v = zeros_bz2(mib);
first.set(None);
v
},
Inherit::Default,
libc::SIGINT,
&ready,
) else {
continue;
};
check(&r, CAUGHT_NO_OUTPUT, &["a.bz2"], &format!("mine={mine}"));
}
}
fn zeros_bz2(mib: usize) -> Vec<u8> {
static UNIT: std::sync::OnceLock<Vec<u8>> = std::sync::OnceLock::new();
let unit = UNIT.get_or_init(|| {
let dir = scratch(&format!("mk-{:?}", std::thread::current().id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("z"), vec![0u8; 4 << 20]).unwrap();
let out = super::compat_harness::output(
Command::new(ours())
.arg0("bzip2")
.args(["-1", "-c", "z"])
.current_dir(&dir)
.stdin(Stdio::null()),
)
.unwrap();
assert!(out.status.success());
out.stdout
});
unit.repeat(mib)
}
}