1use std::collections::BTreeMap;
6use std::ffi::{OsStr, OsString};
7use std::io::{self, IsTerminal};
8use std::path::{Path, PathBuf};
9use std::process::ExitCode;
10
11pub mod bzip2;
12pub mod bzip2recover;
13pub mod gzip;
14mod host;
15pub mod links;
16pub mod xz;
17pub mod zstd;
18
19#[doc(hidden)]
20pub use host::InputReads;
21
22#[doc(hidden)]
25pub type CountedRun = (i32, Vec<u8>, Vec<u8>, BTreeMap<PathBuf, InputReads>);
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum Family {
31 Bzip2,
33 Gzip,
36 Xz,
38 Zstd,
40}
41
42const NAMES: &[(&str, Family)] = &[
45 ("bzip2", Family::Bzip2),
46 ("bunzip2", Family::Bzip2),
47 ("bzcat", Family::Bzip2),
48 ("bzip2recover", Family::Bzip2),
49 ("gzip", Family::Gzip),
50 ("gunzip", Family::Gzip),
51 ("zcat", Family::Gzip),
52 ("gzcat", Family::Gzip),
53 ("xz", Family::Xz),
54 ("unxz", Family::Xz),
55 ("xzcat", Family::Xz),
56 ("lzma", Family::Xz),
57 ("unlzma", Family::Xz),
58 ("lzcat", Family::Xz),
59 ("zstd", Family::Zstd),
60 ("unzstd", Family::Zstd),
61 ("zstdcat", Family::Zstd),
62 ("zstdmt", Family::Zstd),
63];
64
65pub fn names() -> &'static [&'static str] {
67 static LIST: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
68 LIST.get_or_init(|| NAMES.iter().map(|&(n, _)| n).collect())
69}
70
71pub fn family_for(argv0: &OsStr) -> Option<(Family, &'static str)> {
75 let file = Path::new(argv0).file_name()?;
76 let file = file.to_str()?;
77 #[cfg(windows)]
78 let file = file.strip_suffix(".exe").unwrap_or(file);
79 NAMES
80 .iter()
81 .find(|(n, _)| *n == file)
82 .map(|&(n, fam)| (fam, n))
83}
84
85pub fn dispatch(args: Vec<OsString>) -> Option<ExitCode> {
90 #[cfg(all(unix, debug_assertions))]
93 if Path::new(args.first()?).file_name() == Some(OsStr::new("__compat-signal-test")) {
94 return Some(host::signal_test(&args[1..]));
95 }
96 let (family, name) = family_for(args.first()?)?;
97 let mut args = args.into_iter();
98 let argv0 = args.next().unwrap_or_default();
99 let rest = args.collect();
100 Some(match family {
101 Family::Bzip2 if name == "bzip2recover" => bzip2recover::run(argv0, rest),
103 Family::Bzip2 => bzip2::run(name, rest),
104 Family::Gzip => gzip::run(name, rest),
105 Family::Xz => xz::run(name, &argv0, rest),
106 Family::Zstd => zstd::run(name, &argv0, rest),
107 })
108}
109
110#[doc(hidden)]
120pub fn run_in(
121 dir: &Path,
122 name: &str,
123 args: Vec<OsString>,
124 stdin: &[u8],
125 gzip_flavour: gzip::Flavour,
126) -> Option<(i32, Vec<u8>, Vec<u8>)> {
127 run_in_counted(dir, name, args, stdin, gzip_flavour).map(|(code, out, err, _)| (code, out, err))
128}
129
130#[doc(hidden)]
136pub fn run_in_counted(
137 dir: &Path,
138 name: &str,
139 args: Vec<OsString>,
140 stdin: &[u8],
141 gzip_flavour: gzip::Flavour,
142) -> Option<CountedRun> {
143 let (family, canonical) = family_for(OsStr::new(name))?;
144 Some(match family {
145 Family::Bzip2 if canonical == "bzip2recover" => {
146 let (code, err) = bzip2recover::run_in(dir, name.as_bytes(), &args);
147 (code, Vec::new(), err, BTreeMap::new())
148 }
149 Family::Bzip2 => {
150 let (code, out, err) = bzip2::run_in(dir, canonical, &args, stdin);
151 (code, out, err, BTreeMap::new())
152 }
153 Family::Gzip => gzip::run_in_counted(gzip_flavour, dir, canonical, &args, stdin, &|_| None),
154 Family::Xz => xz::run_in_counted(dir, canonical, &args, stdin, &|_| None),
155 Family::Zstd => zstd::run_in_counted(dir, canonical, &args, stdin, &|_| None),
156 })
157}
158
159pub(crate) fn stdin_is_tty() -> bool {
161 io::stdin().is_terminal()
162}
163
164pub(crate) fn stdout_is_tty() -> bool {
166 io::stdout().is_terminal()
167}
168
169pub(crate) fn copy_metadata_from(meta: &std::fs::Metadata, to: &Path) -> io::Result<()> {
178 copy_metadata_with_mtime(meta, to, None)
179}
180
181pub(crate) fn copy_metadata_with_mtime(
184 meta: &std::fs::Metadata,
185 to: &Path,
186 mtime: Option<std::time::SystemTime>,
187) -> io::Result<()> {
188 let mut times = std::fs::FileTimes::new();
189 if let Ok(t) = meta.accessed() {
190 times = times.set_accessed(t);
191 }
192 if let Some(t) = mtime.or_else(|| meta.modified().ok()) {
193 times = times.set_modified(t);
194 }
195 std::fs::File::options()
196 .write(true)
197 .open(to)?
198 .set_times(times)?;
199 #[cfg(unix)]
200 {
201 use std::os::unix::fs::MetadataExt;
202 match std::os::unix::fs::chown(to, Some(meta.uid()), Some(meta.gid())) {
203 Err(e) if e.kind() != io::ErrorKind::PermissionDenied => return Err(e),
204 _ => {}
205 }
206 }
207 std::fs::set_permissions(to, meta.permissions())
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 const G: gzip::Flavour = gzip::Flavour::Gnu;
217
218 #[test]
221 fn fuzz_names_match_the_dispatch_table() {
222 assert_eq!(stuffr::testing::COMPAT_NAMES, names());
223 }
224
225 #[test]
226 fn compat_names_are_recognised_by_file_name_only() {
227 use std::ffi::OsStr;
228 assert_eq!(family_for(OsStr::new("bzip2")).map(|f| f.1), Some("bzip2"));
229 assert_eq!(
230 family_for(OsStr::new("/usr/local/bin/bunzip2")).map(|f| f.1),
231 Some("bunzip2")
232 );
233 assert_eq!(family_for(OsStr::new("bzcat")).map(|f| f.1), Some("bzcat"));
234 assert_eq!(
235 family_for(OsStr::new("bzip2recover")).map(|f| f.1),
236 Some("bzip2recover")
237 );
238 assert!(family_for(OsStr::new("stuffr")).is_none());
239 assert!(family_for(OsStr::new("/x/bzip2/stuffr")).is_none());
240 assert!(family_for(OsStr::new("bzip2x")).is_none());
241 #[cfg(windows)]
242 assert_eq!(
243 family_for(OsStr::new("bzip2.exe")).map(|f| f.1),
244 Some("bzip2")
245 );
246 }
247
248 #[test]
249 fn stuffr_itself_does_not_dispatch() {
250 assert!(dispatch(vec!["stuffr".into(), "list".into()]).is_none());
251 }
252
253 #[test]
254 fn dispatch_ignores_an_empty_argv() {
255 assert!(dispatch(Vec::new()).is_none());
256 }
257
258 #[cfg(unix)]
259 #[test]
260 fn dispatch_ignores_a_non_utf8_argv0() {
261 use std::os::unix::ffi::OsStrExt;
262 let bad = OsStr::from_bytes(b"bzip2\xff").to_os_string();
263 assert!(dispatch(vec![bad]).is_none());
264 let bad = OsStr::from_bytes(b"/x/\xffbzip2").to_os_string();
265 assert!(dispatch(vec![bad]).is_none());
266 }
267
268 #[test]
271 fn run_in_works_inside_its_directory() {
272 let dir = std::env::temp_dir().join(format!("stuffr-run-in-{}", std::process::id()));
273 let _ = std::fs::remove_dir_all(&dir);
274 std::fs::create_dir_all(&dir).unwrap();
275 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
276 let (code, out, err) =
277 run_in(&dir, "bzip2", vec!["-k".into(), "a".into()], b"", G).unwrap();
278 assert_eq!(
279 (code, out.len()),
280 (0, 0),
281 "{}",
282 String::from_utf8_lossy(&err)
283 );
284 let packed = std::fs::read(dir.join("a.bz2")).unwrap();
285 assert!(packed.starts_with(b"BZh9"));
286 let (code, out, _) = run_in(&dir, "bzcat", vec![], &packed, G).unwrap();
287 assert_eq!((code, out.as_slice()), (0, &b"hello hello hello\n"[..]));
288 let (code, out, err) = run_in(&dir, "bzip2recover", vec!["a.bz2".into()], b"", G).unwrap();
289 assert_eq!(
290 (code, out.len()),
291 (0, 0),
292 "{}",
293 String::from_utf8_lossy(&err)
294 );
295 assert!(dir.join("rec00001a.bz2").exists());
296 let (code, _, err) = run_in(&dir, "bunzip2", vec![], b"BZh9garbage", G).unwrap();
298 assert_eq!(code, 2, "{}", String::from_utf8_lossy(&err));
299 assert!(run_in(&dir, "stuffr", vec![], b"", G).is_none());
300 let _ = std::fs::remove_dir_all(&dir);
301 }
302
303 #[test]
308 fn run_in_counted_counts_the_gzip_familys_inputs() {
309 let dir = std::env::temp_dir().join(format!("stuffr-run-in-count-{}", std::process::id()));
310 for flavour in [gzip::Flavour::Gnu, gzip::Flavour::Bsd] {
311 let _ = std::fs::remove_dir_all(&dir);
312 std::fs::create_dir_all(&dir).unwrap();
313 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
314 let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
315 let (code, _, err, reads) =
316 run_in_counted(&dir, "gzip", args(&["-k", "a"]), b"", flavour).unwrap();
317 assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
318 let a = reads[&dir.join("a")];
319 assert_eq!((a.opens, a.bytes, a.len), (1, 18, 18), "{flavour:?}");
320 assert_eq!(reads.len(), 1, "{reads:?}");
321
322 let len = std::fs::metadata(dir.join("a.gz")).unwrap().len();
323 for mode in [&["-t", "a.gz", "a.gz"][..], &["-l", "a.gz"]] {
324 let (code, _, err, reads) =
325 run_in_counted(&dir, "gzip", args(mode), b"", flavour).unwrap();
326 assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
327 let gz = reads[&dir.join("a.gz")];
328 assert_eq!(gz.opens, mode.len() as u64 - 1, "{flavour:?} {mode:?}");
329 assert_eq!(gz.len, len);
330 assert!(gz.bytes > 0 && gz.bytes <= 2 * len * gz.opens, "{gz:?}");
331 }
332 let (_, _, _, reads) =
333 run_in_counted(&dir, "bzip2", args(&["-k", "a"]), b"", flavour).unwrap();
334 assert!(reads.is_empty(), "{reads:?}");
335 }
336 let _ = std::fs::remove_dir_all(&dir);
337 }
338
339 #[test]
343 fn run_in_counted_counts_the_zstd_familys_inputs() {
344 let dir = std::env::temp_dir().join(format!("stuffr-run-in-zstd-{}", std::process::id()));
345 let _ = std::fs::remove_dir_all(&dir);
346 std::fs::create_dir_all(&dir).unwrap();
347 let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
348 let run = |a: &[&str]| {
349 let (code, _, err, reads) =
350 run_in_counted(&dir, "zstd", args(a), b"", gzip::Flavour::Gnu).unwrap();
351 assert_eq!(code, 0, "{a:?}: {}", String::from_utf8_lossy(&err));
352 reads
353 };
354 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
355 std::fs::write(dir.join("d.dict"), b"a raw dictionary\n").unwrap();
356 std::fs::write(dir.join("l"), b"a\n").unwrap();
357
358 let reads = run(&["-q", "-k", "a"]);
359 assert_eq!(
360 reads[&dir.join("a")],
361 InputReads {
362 opens: 1,
363 bytes: 18,
364 len: 18
365 }
366 );
367 let zst = std::fs::metadata(dir.join("a.zst")).unwrap().len();
368
369 let reads = run(&["-d", "-c", "-D", "d.dict", "a.zst"]);
370 let dict = reads[&dir.join("d.dict")];
371 assert_eq!(
372 dict,
373 InputReads {
374 opens: 1,
375 bytes: 17,
376 len: 17
377 }
378 );
379 assert_eq!(reads[&dir.join("a.zst")].len, zst);
380
381 let reads = run(&["-c", "--filelist=l"]);
382 assert_eq!(
383 reads[&dir.join("l")],
384 InputReads {
385 opens: 1,
386 bytes: 2,
387 len: 2
388 }
389 );
390 assert_eq!(reads[&dir.join("a")].opens, 1);
391 let _ = std::fs::remove_dir_all(&dir);
392 }
393
394 #[test]
396 fn run_in_caps_what_a_run_writes() {
397 let dir = std::env::temp_dir().join(format!("stuffr-run-in-cap-{}", std::process::id()));
398 let _ = std::fs::remove_dir_all(&dir);
399 std::fs::create_dir_all(&dir).unwrap();
400 let big = vec![0u8; 9 * 1024 * 1024];
401 let (code, packed, _) = run_in(&dir, "bzip2", vec![], &big, G).unwrap();
402 assert_eq!(code, 0);
403 let (code, out, err) = run_in(&dir, "bzcat", vec![], &packed, G).unwrap();
404 let err = String::from_utf8_lossy(&err);
405 assert_eq!(code, 1, "{err}");
406 let cap = 8 * 1024 * 1024;
409 assert!(
410 out.len() <= cap && out.len() > cap - 64 * 1024,
411 "{}",
412 out.len()
413 );
414 assert!(err.contains("I/O or other error"), "{err}");
415 let (code, out, err) = run_in(&dir, "bzip2", vec!["-t".into()], &packed, G).unwrap();
417 let err = String::from_utf8_lossy(&err);
418 assert_eq!((code, out.len()), (1, 0), "{err}");
419 assert!(err.contains("I/O or other error"), "{err}");
420 let _ = std::fs::remove_dir_all(&dir);
421 }
422
423 #[test]
424 fn names_lists_the_table() {
425 assert_eq!(
426 names(),
427 [
428 "bzip2",
429 "bunzip2",
430 "bzcat",
431 "bzip2recover",
432 "gzip",
433 "gunzip",
434 "zcat",
435 "gzcat",
436 "xz",
437 "unxz",
438 "xzcat",
439 "lzma",
440 "unlzma",
441 "lzcat",
442 "zstd",
443 "unzstd",
444 "zstdcat",
445 "zstdmt"
446 ]
447 );
448 }
449
450 #[test]
451 fn copy_metadata_carries_mtime_and_permissions() {
452 let dir = std::env::temp_dir().join(format!("stuffr-meta-{}", std::process::id()));
453 std::fs::create_dir_all(&dir).unwrap();
454 let (a, b) = (dir.join("a"), dir.join("b"));
455 std::fs::write(&a, b"x").unwrap();
456 std::fs::write(&b, b"y").unwrap();
457 let old = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
458 std::fs::File::options()
459 .write(true)
460 .open(&a)
461 .unwrap()
462 .set_modified(old)
463 .unwrap();
464 #[cfg(unix)]
465 {
466 use std::os::unix::fs::PermissionsExt;
467 std::fs::set_permissions(&a, std::fs::Permissions::from_mode(0o640)).unwrap();
468 }
469 copy_metadata_from(&std::fs::metadata(&a).unwrap(), &b).unwrap();
470 let (ma, mb) = (
471 std::fs::metadata(&a).unwrap(),
472 std::fs::metadata(&b).unwrap(),
473 );
474 assert_eq!(ma.modified().unwrap(), mb.modified().unwrap());
475 assert_eq!(ma.permissions(), mb.permissions());
476 let _ = std::fs::remove_dir_all(&dir);
477 }
478}