use std::collections::BTreeMap;
use std::ffi::OsString;
use std::io::{self, Write};
use std::path::Path;
use std::process::ExitCode;
use std::sync::atomic::AtomicU64;
use std::sync::{Arc, Mutex};
use super::CountedRun;
use super::host::{Host, SANDBOX_OUTPUT_CAP, Sandbox, reset_sigpipe};
mod bsd;
mod gnu;
#[allow(dead_code)]
pub(crate) mod member;
#[allow(dead_code)]
pub(crate) mod sniff;
pub use sniff::Flavour;
const FLAVOR_VAR: &str = "STUFFR_GZIP_FLAVOR";
pub fn default_flavour() -> Flavour {
if cfg!(any(
target_vendor = "apple",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
)) {
Flavour::Bsd
} else {
Flavour::Gnu
}
}
fn flavour_from(value: Option<OsString>) -> Result<Flavour, OsString> {
match value {
None => Ok(default_flavour()),
Some(v) if v.is_empty() => Ok(default_flavour()),
Some(v) if v == "gnu" => Ok(Flavour::Gnu),
Some(v) if v == "bsd" => Ok(Flavour::Bsd),
Some(v) => Err(v),
}
}
pub fn run(name: &'static str, args: Vec<OsString>) -> ExitCode {
reset_sigpipe();
let env = |k: &str| std::env::var_os(k);
let mut err = io::stderr();
let flavour = match flavour_from(env(FLAVOR_VAR)) {
Ok(f) => f,
Err(v) => {
let _ = writeln!(
err,
"{name}: {FLAVOR_VAR} must be gnu or bsd, not '{}'",
v.to_string_lossy()
);
return ExitCode::from(1);
}
};
let argv0 = std::env::args_os()
.next()
.map(|a| a.as_encoded_bytes().to_vec())
.unwrap_or_else(|| name.as_bytes().to_vec());
let code = main(
flavour,
name,
&argv0,
&args,
&env,
&mut Host::Real,
&mut err,
);
ExitCode::from(code)
}
fn main(
flavour: Flavour,
name: &'static str,
argv0: &[u8],
args: &[OsString],
env: &dyn Fn(&str) -> Option<OsString>,
host: &mut Host,
err: &mut dyn Write,
) -> u8 {
match flavour {
Flavour::Gnu => gnu::main(name, argv0, args, env, host, err),
Flavour::Bsd => bsd::main(name, args, env, host, err),
}
}
#[doc(hidden)]
pub fn run_in(
flavour: Flavour,
dir: &Path,
name: &'static str,
args: &[OsString],
stdin: &[u8],
env: &dyn Fn(&str) -> Option<OsString>,
) -> (i32, Vec<u8>, Vec<u8>) {
let (code, out, err, _) = run_in_counted(flavour, dir, name, args, stdin, env);
(code, out, err)
}
#[doc(hidden)]
pub fn run_in_counted(
flavour: Flavour,
dir: &Path,
name: &'static str,
args: &[OsString],
stdin: &[u8],
env: &dyn Fn(&str) -> Option<OsString>,
) -> CountedRun {
let stdout = Arc::new(Mutex::new(Vec::new()));
let reads = Arc::new(Mutex::new(BTreeMap::new()));
let mut host = Host::Sandbox(Sandbox {
dir: dir.to_path_buf(),
stdin: Some(stdin.to_vec()),
stdout: stdout.clone(),
budget: Arc::new(AtomicU64::new(SANDBOX_OUTPUT_CAP)),
reads: reads.clone(),
});
let mut err = Vec::new();
let code = main(
flavour,
name,
name.as_bytes(),
args,
env,
&mut host,
&mut err,
);
drop(host);
let out = std::mem::take(&mut *stdout.lock().unwrap_or_else(|p| p.into_inner()));
let reads = std::mem::take(&mut *reads.lock().unwrap_or_else(|p| p.into_inner()));
(i32::from(code), out, err, reads)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_flavour_variable_selects_or_is_refused() {
assert_eq!(flavour_from(None), Ok(default_flavour()));
assert_eq!(flavour_from(Some("".into())), Ok(default_flavour()));
assert_eq!(flavour_from(Some("gnu".into())), Ok(Flavour::Gnu));
assert_eq!(flavour_from(Some("bsd".into())), Ok(Flavour::Bsd));
assert_eq!(flavour_from(Some("GNU".into())), Err("GNU".into()));
assert_eq!(flavour_from(Some(" gnu".into())), Err(" gnu".into()));
}
const SMALL_STACK: usize = 256 * 1024;
#[cfg(unix)]
#[test]
fn recursion_depth_costs_no_stack() {
let root = std::env::temp_dir().join(format!("stuffr-deep-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
let root = root.canonicalize().unwrap();
let path_max = if cfg!(target_os = "macos") {
1024
} else {
4096
};
let room = path_max - 1 - root.as_os_str().len() - 64;
let levels = (room / 2).min(1000);
assert!(levels >= 400, "only {levels} levels fit under {root:?}");
let deep = root.join(vec!["d"; levels].join("/"));
let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
gz.write_all(b"deep\n").unwrap();
let gz = gz.finish().unwrap();
for flavour in [Flavour::Gnu, Flavour::Bsd] {
std::fs::create_dir_all(&deep).unwrap();
std::fs::write(deep.join("leaf.gz"), &gz).unwrap();
let skipped = if flavour == Flavour::Bsd { 1 } else { 0 };
for (name, args, want) in [
("gzip", &["-r", "d"][..], skipped),
("gzip", &["-tr", "d"], 0),
("gunzip", &["-r", "d"], 0),
] {
let dir = root.clone();
let argv: Vec<OsString> = args.iter().map(OsString::from).collect();
let (code, _, err) = std::thread::Builder::new()
.name("recursion_depth_costs_no_stack".into())
.stack_size(SMALL_STACK)
.spawn(move || run_in(flavour, &dir, name, &argv, b"", &|_| None))
.unwrap()
.join()
.expect("the walk panicked");
let err = String::from_utf8_lossy(&err);
assert_eq!(code, want, "{flavour:?} {name} {args:?}: {err}");
}
assert_eq!(std::fs::read(deep.join("leaf")).unwrap(), b"deep\n");
assert!(!deep.join("leaf.gz").exists());
std::fs::remove_dir_all(root.join("d")).unwrap();
}
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn the_default_flavour_follows_the_platform() {
let bsd = cfg!(any(
target_os = "macos",
target_os = "ios",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
));
assert_eq!(
default_flavour(),
if bsd { Flavour::Bsd } else { Flavour::Gnu }
);
}
}