use std::cell::Cell;
use std::io::Read;
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::path::Path;
use std::process::{Command, ExitStatus, Stdio};
use std::sync::{Mutex, MutexGuard, OnceLock};
use std::time::{Duration, Instant};
pub struct Interrupted {
pub status: ExitStatus,
pub stderr: Vec<u8>,
pub stdout: Vec<u8>,
pub left: Vec<String>,
}
#[derive(Clone, Copy)]
pub enum Inherit {
Default,
IgnoreInt,
}
fn big_block() -> &'static [u8] {
static BLOCK: OnceLock<Vec<u8>> = OnceLock::new();
BLOCK.get_or_init(|| super::random(4 << 20, 0x5157_0013))
}
fn big_pieces(mib: usize) -> impl Iterator<Item = &'static [u8]> {
let block = big_block();
let mut left = mib << 20;
std::iter::from_fn(move || {
let n = left.min(block.len());
left -= n;
(n > 0).then(|| &block[..n])
})
}
pub fn big_input(mib: usize) -> Vec<u8> {
assert!(mib <= 256, "big_input holds at most 256 MiB, not {mib}");
let mut v = Vec::with_capacity(mib << 20);
for piece in big_pieces(mib) {
v.extend_from_slice(piece);
}
v
}
pub fn big_input_through(exe: &str, args: &[&str], mib: usize) -> Option<Vec<u8>> {
use std::io::Write;
if !super::require_reference(Path::new(exe)) {
return None;
}
let mut child = super::spawn(
Command::new(exe)
.args(args)
.env_clear()
.stdin(Stdio::piped())
.stdout(Stdio::piped()),
)
.unwrap();
let mut si = child.stdin.take().unwrap();
let writer = std::thread::spawn(move || {
for piece in big_pieces(mib) {
if si.write_all(piece).is_err() {
break;
}
}
});
let mut out = Vec::with_capacity((mib << 20) + (mib << 10) + 4096);
child.stdout.take().unwrap().read_to_end(&mut out).unwrap();
let status = child.wait().unwrap();
writer.join().unwrap();
assert!(status.success(), "{exe} {args:?}");
Some(out)
}
const HANG_DEADLINE: Duration = Duration::from_secs(30);
const IGNORED_SIGNAL_DEADLINE: Duration = Duration::from_secs(600);
thread_local! {
static REFERENCE_HUNG: Cell<bool> = const { Cell::new(false) };
}
pub fn take_reference_hung() -> bool {
REFERENCE_HUNG.with(|h| h.replace(false))
}
fn is_stuffr(exe: &Path) -> bool {
match (
std::fs::canonicalize(exe),
std::fs::canonicalize(env!("CARGO_BIN_EXE_stuffr")),
) {
(Ok(a), Ok(b)) => a == b,
_ => true,
}
}
static INTERRUPT_LOCK: Mutex<()> = Mutex::new(());
pub fn interrupt_lock() -> MutexGuard<'static, ()> {
INTERRUPT_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
pub fn fast_size(mib: usize) -> usize {
(mib * 3).min(256)
}
pub fn listing(dir: &Path) -> Vec<String> {
let mut v: Vec<String> = std::fs::read_dir(dir)
.unwrap()
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
v.sort();
v
}
pub fn interrupt(
exe: &Path,
name: &str,
args: &[&str],
dir: &Path,
inherit: Inherit,
sig: i32,
ready: &dyn Fn(&str) -> bool,
) -> Option<Interrupted> {
interrupt_env(exe, name, args, dir, &[], inherit, sig, ready)
}
#[allow(clippy::too_many_arguments)]
pub fn interrupt_env(
exe: &Path,
name: &str,
args: &[&str],
dir: &Path,
env: &[(&str, &str)],
inherit: Inherit,
sig: i32,
ready: &dyn Fn(&str) -> bool,
) -> Option<Interrupted> {
interrupt_when(
exe,
name,
args,
dir,
env,
inherit,
sig,
Ready::Listing(ready),
)
}
#[allow(clippy::too_many_arguments)]
pub fn interrupt_listing_stdout(
exe: &Path,
name: &str,
args: &[&str],
dir: &Path,
env: &[(&str, &str)],
inherit: Inherit,
sig: i32,
ready: &dyn Fn(&str) -> bool,
) -> Option<Interrupted> {
interrupt_when(
exe,
name,
args,
dir,
env,
inherit,
sig,
Ready::ListingStdout(ready),
)
}
#[allow(clippy::too_many_arguments)]
pub fn interrupt_once_stdout_has_data(
exe: &Path,
name: &str,
args: &[&str],
dir: &Path,
env: &[(&str, &str)],
inherit: Inherit,
sig: i32,
) -> Option<Interrupted> {
interrupt_when(exe, name, args, dir, env, inherit, sig, Ready::Stdout)
}
enum Ready<'a> {
Listing(&'a dyn Fn(&str) -> bool),
ListingStdout(&'a dyn Fn(&str) -> bool),
Stdout,
}
#[allow(clippy::too_many_arguments)]
fn interrupt_when(
exe: &Path,
name: &str,
args: &[&str],
dir: &Path,
env: &[(&str, &str)],
inherit: Inherit,
sig: i32,
ready: Ready,
) -> Option<Interrupted> {
let mut cmd = Command::new(exe);
cmd.arg0(name).args(args).current_dir(dir);
cmd.envs(env.iter().copied());
let stdout = match ready {
Ready::Listing(_) => Stdio::null(),
Ready::Stdout | Ready::ListingStdout(_) => Stdio::piped(),
};
let capture = matches!(ready, Ready::ListingStdout(_));
cmd.stdin(Stdio::null())
.stdout(stdout)
.stderr(Stdio::piped());
unsafe {
cmd.pre_exec(move || {
let int = match inherit {
Inherit::Default => libc::SIG_DFL,
Inherit::IgnoreInt => libc::SIG_IGN,
};
libc::signal(libc::SIGINT, int);
for s in [libc::SIGTERM, libc::SIGHUP, libc::SIGQUIT, libc::SIGPIPE] {
libc::signal(s, libc::SIG_DFL);
}
Ok(())
});
}
REFERENCE_HUNG.with(|h| h.set(false));
let mut child = super::spawn(&mut cmd).expect("spawn");
let wrote = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let drain = child.stdout.take().map(|mut out| {
let wrote = wrote.clone();
std::thread::spawn(move || {
let mut kept = Vec::new();
let mut buf = [0u8; 65536];
while let Ok(n) = out.read(&mut buf) {
if n == 0 {
break;
}
if capture {
kept.extend_from_slice(&buf[..n]);
}
wrote.store(true, std::sync::atomic::Ordering::SeqCst);
}
kept
})
});
let deadline = Instant::now() + Duration::from_secs(30);
loop {
if child.try_wait().unwrap().is_some() {
return None;
}
let is_ready = match ready {
Ready::Listing(f) | Ready::ListingStdout(f) => listing(dir).iter().any(|n| f(n)),
Ready::Stdout => wrote.load(std::sync::atomic::Ordering::SeqCst),
};
if is_ready {
break;
}
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
panic!("{name} {args:?}: never became ready");
}
std::thread::sleep(Duration::from_millis(2));
}
let mut pipe = child.stderr.take().expect("piped stderr");
let reader = std::thread::spawn(move || {
let mut v = Vec::new();
let _ = pipe.read_to_end(&mut v);
v
});
unsafe { libc::kill(child.id() as i32, sig) };
let hang_at = Instant::now()
+ match inherit {
Inherit::Default => HANG_DEADLINE,
Inherit::IgnoreInt => IGNORED_SIGNAL_DEADLINE,
};
let status = loop {
if let Some(st) = child.try_wait().unwrap() {
break Some(st);
}
if Instant::now() >= hang_at {
break None;
}
std::thread::sleep(Duration::from_millis(5));
};
let Some(status) = status else {
let _ = child.kill();
let _ = child.wait();
let stderr = reader.join().unwrap_or_default();
let note = format!(
"{name} {args:?}: hung after the signal ({sig}); stderr so far: {:?}",
String::from_utf8_lossy(&stderr)
);
assert!(!is_stuffr(exe), "{note}");
eprintln!("note: reference {note}; attempt counted as a miss");
REFERENCE_HUNG.with(|h| h.set(true));
return None;
};
let stderr = reader.join().unwrap_or_default();
let stdout = drain.and_then(|d| d.join().ok()).unwrap_or_default();
Some(Interrupted {
status,
stderr,
stdout,
left: listing(dir),
})
}
#[allow(clippy::too_many_arguments)]
pub fn interrupt_prepared(
exe: &Path,
name: &str,
args: &[&str],
dir: &Path,
env: &[(&str, &str)],
prepare: &dyn Fn(usize),
inherit: Inherit,
sig: i32,
ready: &dyn Fn(&str) -> bool,
) -> Option<Interrupted> {
let mut hung = false;
for mib in [64, 256] {
let _ = std::fs::remove_dir_all(dir);
std::fs::create_dir_all(dir).unwrap();
prepare(mib);
match interrupt_when(
exe,
name,
args,
dir,
env,
inherit,
sig,
Ready::ListingStdout(ready),
) {
Some(r) if !r.status.success() => return Some(r),
_ => hung |= take_reference_hung(),
}
}
if hung {
eprintln!("note: {name} {args:?}: the reference hung after the signal; comparison skipped");
return None;
}
panic!("{name} {args:?}: finished before the signal even at the larger size");
}
#[allow(clippy::too_many_arguments)]
pub fn interrupt_with_input(
exe: &Path,
name: &str,
args: &[&str],
dir: &Path,
file: &str,
input: &dyn Fn(usize) -> Vec<u8>,
inherit: Inherit,
sig: i32,
ready: &dyn Fn(&str) -> bool,
) -> Option<Interrupted> {
interrupt_with_input_env(exe, name, args, dir, &[], file, input, inherit, sig, ready)
}
#[allow(clippy::too_many_arguments)]
pub fn interrupt_with_input_env(
exe: &Path,
name: &str,
args: &[&str],
dir: &Path,
env: &[(&str, &str)],
file: &str,
input: &dyn Fn(usize) -> Vec<u8>,
inherit: Inherit,
sig: i32,
ready: &dyn Fn(&str) -> bool,
) -> Option<Interrupted> {
let mut hung = false;
for mib in [64, 256] {
let _ = std::fs::remove_dir_all(dir);
std::fs::create_dir_all(dir).unwrap();
std::fs::write(dir.join(file), input(mib)).unwrap();
match interrupt_env(exe, name, args, dir, env, inherit, sig, ready) {
Some(r) if !r.status.success() => return Some(r),
_ => hung |= take_reference_hung(),
}
}
if hung {
eprintln!("note: {name} {args:?}: the reference hung after the signal; comparison skipped");
return None;
}
panic!("{name} {args:?}: finished before the signal even at the larger size");
}
pub fn killed_by(r: &Interrupted, sig: i32) -> bool {
r.status.signal() == Some(sig)
}
pub fn stored_gzip_of_big(mib: usize) -> Vec<u8> {
let block = big_block();
let len = mib << 20;
let mut crc = flate2::Crc::new();
let mut v = Vec::with_capacity(stored_gzip_len(len));
v.extend_from_slice(&[0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 3]);
if len == 0 {
v.extend_from_slice(&[1, 0, 0, 0xff, 0xff]);
}
let mut at = 0;
while at < len {
let n = (len - at).min(65535);
v.push(u8::from(at + n == len));
v.extend_from_slice(&(n as u16).to_le_bytes());
v.extend_from_slice(&(!(n as u16)).to_le_bytes());
let mut done = 0;
while done < n {
let off = (at + done) % block.len();
let piece = &block[off..block.len().min(off + n - done)];
crc.update(piece);
v.extend_from_slice(piece);
done += piece.len();
}
at += n;
}
v.extend_from_slice(&crc.sum().to_le_bytes());
v.extend_from_slice(&(len as u32).to_le_bytes());
v
}
fn stored_gzip_len(n: usize) -> usize {
10 + 5 * n.div_ceil(65535).max(1) + n + 8
}
pub fn stored_gzip(data: &[u8]) -> Vec<u8> {
let mut v = Vec::with_capacity(stored_gzip_len(data.len()));
v.extend_from_slice(&[0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 3]);
let mut chunks = data.chunks(65535).peekable();
if chunks.peek().is_none() {
v.extend_from_slice(&[1, 0, 0, 0xff, 0xff]);
}
while let Some(c) = chunks.next() {
v.push(u8::from(chunks.peek().is_none()));
v.extend_from_slice(&(c.len() as u16).to_le_bytes());
v.extend_from_slice(&(!(c.len() as u16)).to_le_bytes());
v.extend_from_slice(c);
}
v.extend_from_slice(&super::crc32(data).to_le_bytes());
v.extend_from_slice(&(data.len() as u32).to_le_bytes());
v
}
pub fn stored_zip(name: &[u8], data: &[u8]) -> Vec<u8> {
let crc = super::crc32(data);
let size = u32::try_from(data.len()).expect("under 4 GiB");
let n = u16::try_from(name.len()).unwrap();
let mut v = Vec::with_capacity(data.len() + 200);
v.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
for x in [20u16, 0, 0, 0, 0x5021] {
v.extend_from_slice(&x.to_le_bytes());
}
for x in [crc, size, size] {
v.extend_from_slice(&x.to_le_bytes());
}
v.extend_from_slice(&n.to_le_bytes());
v.extend_from_slice(&0u16.to_le_bytes());
v.extend_from_slice(name);
v.extend_from_slice(data);
let cd_at = u32::try_from(v.len()).unwrap();
v.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
for x in [0x031eu16, 20, 0, 0, 0, 0x5021] {
v.extend_from_slice(&x.to_le_bytes());
}
for x in [crc, size, size] {
v.extend_from_slice(&x.to_le_bytes());
}
for x in [n, 0, 0, 0, 0] {
v.extend_from_slice(&x.to_le_bytes());
}
v.extend_from_slice(&(0o100644u32 << 16).to_le_bytes());
v.extend_from_slice(&0u32.to_le_bytes());
v.extend_from_slice(name);
let cd_len = u32::try_from(v.len()).unwrap() - cd_at;
v.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
for x in [0u16, 0, 1, 1] {
v.extend_from_slice(&x.to_le_bytes());
}
v.extend_from_slice(&cd_len.to_le_bytes());
v.extend_from_slice(&cd_at.to_le_bytes());
v.extend_from_slice(&0u16.to_le_bytes());
v
}
pub fn has_data<'a>(dir: &'a Path, named: &'a dyn Fn(&str) -> bool) -> impl Fn(&str) -> bool + 'a {
move |n| named(n) && std::fs::metadata(dir.join(n)).is_ok_and(|m| m.len() > 0)
}