use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::io::{self, IsTerminal};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
pub mod bzip2;
pub mod bzip2recover;
pub mod gzip;
mod host;
pub mod links;
#[doc(hidden)]
pub use host::InputReads;
#[doc(hidden)]
pub type CountedRun = (i32, Vec<u8>, Vec<u8>, BTreeMap<PathBuf, InputReads>);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Family {
Bzip2,
Gzip,
}
const NAMES: &[(&str, Family)] = &[
("bzip2", Family::Bzip2),
("bunzip2", Family::Bzip2),
("bzcat", Family::Bzip2),
("bzip2recover", Family::Bzip2),
("gzip", Family::Gzip),
("gunzip", Family::Gzip),
("zcat", Family::Gzip),
("gzcat", Family::Gzip),
];
pub fn names() -> &'static [&'static str] {
static LIST: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
LIST.get_or_init(|| NAMES.iter().map(|&(n, _)| n).collect())
}
pub fn family_for(argv0: &OsStr) -> Option<(Family, &'static str)> {
let file = Path::new(argv0).file_name()?;
let file = file.to_str()?;
#[cfg(windows)]
let file = file.strip_suffix(".exe").unwrap_or(file);
NAMES
.iter()
.find(|(n, _)| *n == file)
.map(|&(n, fam)| (fam, n))
}
pub fn dispatch(args: Vec<OsString>) -> Option<ExitCode> {
let (family, name) = family_for(args.first()?)?;
let mut args = args.into_iter();
let argv0 = args.next().unwrap_or_default();
let rest = args.collect();
Some(match family {
Family::Bzip2 if name == "bzip2recover" => bzip2recover::run(argv0, rest),
Family::Bzip2 => bzip2::run(name, rest),
Family::Gzip => gzip::run(name, rest),
})
}
#[doc(hidden)]
pub fn run_in(
dir: &Path,
name: &str,
args: Vec<OsString>,
stdin: &[u8],
gzip_flavour: gzip::Flavour,
) -> Option<(i32, Vec<u8>, Vec<u8>)> {
run_in_counted(dir, name, args, stdin, gzip_flavour).map(|(code, out, err, _)| (code, out, err))
}
#[doc(hidden)]
pub fn run_in_counted(
dir: &Path,
name: &str,
args: Vec<OsString>,
stdin: &[u8],
gzip_flavour: gzip::Flavour,
) -> Option<CountedRun> {
let (family, canonical) = family_for(OsStr::new(name))?;
Some(match family {
Family::Bzip2 if canonical == "bzip2recover" => {
let (code, err) = bzip2recover::run_in(dir, name.as_bytes(), &args);
(code, Vec::new(), err, BTreeMap::new())
}
Family::Bzip2 => {
let (code, out, err) = bzip2::run_in(dir, canonical, &args, stdin);
(code, out, err, BTreeMap::new())
}
Family::Gzip => gzip::run_in_counted(gzip_flavour, dir, canonical, &args, stdin, &|_| None),
})
}
pub(crate) fn stdin_is_tty() -> bool {
io::stdin().is_terminal()
}
pub(crate) fn stdout_is_tty() -> bool {
io::stdout().is_terminal()
}
pub(crate) fn copy_metadata_from(meta: &std::fs::Metadata, to: &Path) -> io::Result<()> {
copy_metadata_with_mtime(meta, to, None)
}
pub(crate) fn copy_metadata_with_mtime(
meta: &std::fs::Metadata,
to: &Path,
mtime: Option<std::time::SystemTime>,
) -> io::Result<()> {
let mut times = std::fs::FileTimes::new();
if let Ok(t) = meta.accessed() {
times = times.set_accessed(t);
}
if let Some(t) = mtime.or_else(|| meta.modified().ok()) {
times = times.set_modified(t);
}
std::fs::File::options()
.write(true)
.open(to)?
.set_times(times)?;
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
match std::os::unix::fs::chown(to, Some(meta.uid()), Some(meta.gid())) {
Err(e) if e.kind() != io::ErrorKind::PermissionDenied => return Err(e),
_ => {}
}
}
std::fs::set_permissions(to, meta.permissions())
}
#[cfg(test)]
mod tests {
use super::*;
const G: gzip::Flavour = gzip::Flavour::Gnu;
#[test]
fn fuzz_names_match_the_dispatch_table() {
assert_eq!(stuffr::testing::COMPAT_NAMES, names());
}
#[test]
fn compat_names_are_recognised_by_file_name_only() {
use std::ffi::OsStr;
assert_eq!(family_for(OsStr::new("bzip2")).map(|f| f.1), Some("bzip2"));
assert_eq!(
family_for(OsStr::new("/usr/local/bin/bunzip2")).map(|f| f.1),
Some("bunzip2")
);
assert_eq!(family_for(OsStr::new("bzcat")).map(|f| f.1), Some("bzcat"));
assert_eq!(
family_for(OsStr::new("bzip2recover")).map(|f| f.1),
Some("bzip2recover")
);
assert!(family_for(OsStr::new("stuffr")).is_none());
assert!(family_for(OsStr::new("/x/bzip2/stuffr")).is_none());
assert!(family_for(OsStr::new("bzip2x")).is_none());
#[cfg(windows)]
assert_eq!(
family_for(OsStr::new("bzip2.exe")).map(|f| f.1),
Some("bzip2")
);
}
#[test]
fn stuffr_itself_does_not_dispatch() {
assert!(dispatch(vec!["stuffr".into(), "list".into()]).is_none());
}
#[test]
fn dispatch_ignores_an_empty_argv() {
assert!(dispatch(Vec::new()).is_none());
}
#[cfg(unix)]
#[test]
fn dispatch_ignores_a_non_utf8_argv0() {
use std::os::unix::ffi::OsStrExt;
let bad = OsStr::from_bytes(b"bzip2\xff").to_os_string();
assert!(dispatch(vec![bad]).is_none());
let bad = OsStr::from_bytes(b"/x/\xffbzip2").to_os_string();
assert!(dispatch(vec![bad]).is_none());
}
#[test]
fn run_in_works_inside_its_directory() {
let dir = std::env::temp_dir().join(format!("stuffr-run-in-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
let (code, out, err) =
run_in(&dir, "bzip2", vec!["-k".into(), "a".into()], b"", G).unwrap();
assert_eq!(
(code, out.len()),
(0, 0),
"{}",
String::from_utf8_lossy(&err)
);
let packed = std::fs::read(dir.join("a.bz2")).unwrap();
assert!(packed.starts_with(b"BZh9"));
let (code, out, _) = run_in(&dir, "bzcat", vec![], &packed, G).unwrap();
assert_eq!((code, out.as_slice()), (0, &b"hello hello hello\n"[..]));
let (code, out, err) = run_in(&dir, "bzip2recover", vec!["a.bz2".into()], b"", G).unwrap();
assert_eq!(
(code, out.len()),
(0, 0),
"{}",
String::from_utf8_lossy(&err)
);
assert!(dir.join("rec00001a.bz2").exists());
let (code, _, err) = run_in(&dir, "bunzip2", vec![], b"BZh9garbage", G).unwrap();
assert_eq!(code, 2, "{}", String::from_utf8_lossy(&err));
assert!(run_in(&dir, "stuffr", vec![], b"", G).is_none());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn run_in_counted_counts_the_gzip_familys_inputs() {
let dir = std::env::temp_dir().join(format!("stuffr-run-in-count-{}", std::process::id()));
for flavour in [gzip::Flavour::Gnu, gzip::Flavour::Bsd] {
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
let (code, _, err, reads) =
run_in_counted(&dir, "gzip", args(&["-k", "a"]), b"", flavour).unwrap();
assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
let a = reads[&dir.join("a")];
assert_eq!((a.opens, a.bytes, a.len), (1, 18, 18), "{flavour:?}");
assert_eq!(reads.len(), 1, "{reads:?}");
let len = std::fs::metadata(dir.join("a.gz")).unwrap().len();
for mode in [&["-t", "a.gz", "a.gz"][..], &["-l", "a.gz"]] {
let (code, _, err, reads) =
run_in_counted(&dir, "gzip", args(mode), b"", flavour).unwrap();
assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
let gz = reads[&dir.join("a.gz")];
assert_eq!(gz.opens, mode.len() as u64 - 1, "{flavour:?} {mode:?}");
assert_eq!(gz.len, len);
assert!(gz.bytes > 0 && gz.bytes <= 2 * len * gz.opens, "{gz:?}");
}
let (_, _, _, reads) =
run_in_counted(&dir, "bzip2", args(&["-k", "a"]), b"", flavour).unwrap();
assert!(reads.is_empty(), "{reads:?}");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn run_in_caps_what_a_run_writes() {
let dir = std::env::temp_dir().join(format!("stuffr-run-in-cap-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let big = vec![0u8; 9 * 1024 * 1024];
let (code, packed, _) = run_in(&dir, "bzip2", vec![], &big, G).unwrap();
assert_eq!(code, 0);
let (code, out, err) = run_in(&dir, "bzcat", vec![], &packed, G).unwrap();
let err = String::from_utf8_lossy(&err);
assert_eq!(code, 1, "{err}");
let cap = 8 * 1024 * 1024;
assert!(
out.len() <= cap && out.len() > cap - 64 * 1024,
"{}",
out.len()
);
assert!(err.contains("I/O or other error"), "{err}");
let (code, out, err) = run_in(&dir, "bzip2", vec!["-t".into()], &packed, G).unwrap();
let err = String::from_utf8_lossy(&err);
assert_eq!((code, out.len()), (1, 0), "{err}");
assert!(err.contains("I/O or other error"), "{err}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn names_lists_the_table() {
assert_eq!(
names(),
[
"bzip2",
"bunzip2",
"bzcat",
"bzip2recover",
"gzip",
"gunzip",
"zcat",
"gzcat"
]
);
}
#[test]
fn copy_metadata_carries_mtime_and_permissions() {
let dir = std::env::temp_dir().join(format!("stuffr-meta-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let (a, b) = (dir.join("a"), dir.join("b"));
std::fs::write(&a, b"x").unwrap();
std::fs::write(&b, b"y").unwrap();
let old = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
std::fs::File::options()
.write(true)
.open(&a)
.unwrap()
.set_modified(old)
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&a, std::fs::Permissions::from_mode(0o640)).unwrap();
}
copy_metadata_from(&std::fs::metadata(&a).unwrap(), &b).unwrap();
let (ma, mb) = (
std::fs::metadata(&a).unwrap(),
std::fs::metadata(&b).unwrap(),
);
assert_eq!(ma.modified().unwrap(), mb.modified().unwrap());
assert_eq!(ma.permissions(), mb.permissions());
let _ = std::fs::remove_dir_all(&dir);
}
}