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 lz4;
17pub mod unzip;
18pub mod xz;
19pub mod zstd;
20
21#[doc(hidden)]
22pub use host::InputReads;
23
24#[doc(hidden)]
27pub type CountedRun = (i32, Vec<u8>, Vec<u8>, BTreeMap<PathBuf, InputReads>);
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum Family {
33 Bzip2,
35 Gzip,
38 Xz,
40 Zstd,
42 Lz4,
44 Unzip,
47}
48
49const NAMES: &[(&str, Family)] = &[
52 ("bzip2", Family::Bzip2),
53 ("bunzip2", Family::Bzip2),
54 ("bzcat", Family::Bzip2),
55 ("bzip2recover", Family::Bzip2),
56 ("gzip", Family::Gzip),
57 ("gunzip", Family::Gzip),
58 ("zcat", Family::Gzip),
59 ("gzcat", Family::Gzip),
60 ("xz", Family::Xz),
61 ("unxz", Family::Xz),
62 ("xzcat", Family::Xz),
63 ("lzma", Family::Xz),
64 ("unlzma", Family::Xz),
65 ("lzcat", Family::Xz),
66 ("zstd", Family::Zstd),
67 ("unzstd", Family::Zstd),
68 ("zstdcat", Family::Zstd),
69 ("zstdmt", Family::Zstd),
70 ("lz4", Family::Lz4),
71 ("unlz4", Family::Lz4),
72 ("lz4cat", Family::Lz4),
73 ("lz4c", Family::Lz4),
74 ("unzip", Family::Unzip),
75 ("zipinfo", Family::Unzip),
76 ("funzip", Family::Unzip),
77];
78
79pub fn names() -> &'static [&'static str] {
81 static LIST: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
82 LIST.get_or_init(|| NAMES.iter().map(|&(n, _)| n).collect())
83}
84
85pub fn family_for(argv0: &OsStr) -> Option<(Family, &'static str)> {
89 let file = Path::new(argv0).file_name()?;
90 let file = file.to_str()?;
91 #[cfg(windows)]
92 let file = file.strip_suffix(".exe").unwrap_or(file);
93 NAMES
94 .iter()
95 .find(|(n, _)| *n == file)
96 .map(|&(n, fam)| (fam, n))
97}
98
99pub fn dispatch(args: Vec<OsString>) -> Option<ExitCode> {
104 #[cfg(all(unix, debug_assertions))]
107 if Path::new(args.first()?).file_name() == Some(OsStr::new("__compat-signal-test")) {
108 return Some(host::signal_test(&args[1..]));
109 }
110 let (family, name) = family_for(args.first()?)?;
111 let mut args = args.into_iter();
112 let argv0 = args.next().unwrap_or_default();
113 let rest = args.collect();
114 Some(match family {
115 Family::Bzip2 if name == "bzip2recover" => bzip2recover::run(argv0, rest),
117 Family::Bzip2 => bzip2::run(name, rest),
118 Family::Gzip => gzip::run(name, rest),
119 Family::Xz => xz::run(name, &argv0, rest),
120 Family::Zstd => zstd::run(name, &argv0, rest),
121 Family::Lz4 => lz4::run(name, &argv0, rest),
122 Family::Unzip => unzip::run(name, &argv0, rest),
123 })
124}
125
126#[doc(hidden)]
138pub fn run_in(
139 dir: &Path,
140 name: &str,
141 args: Vec<OsString>,
142 stdin: &[u8],
143 gzip_flavour: gzip::Flavour,
144) -> Option<(i32, Vec<u8>, Vec<u8>)> {
145 run_in_counted(
146 dir,
147 name,
148 args,
149 stdin,
150 gzip_flavour,
151 unzip::default_flavour(),
152 )
153 .map(|(code, out, err, _)| (code, out, err))
154}
155
156#[doc(hidden)]
165pub fn run_in_counted(
166 dir: &Path,
167 name: &str,
168 args: Vec<OsString>,
169 stdin: &[u8],
170 gzip_flavour: gzip::Flavour,
171 unzip_flavour: unzip::Flavour,
172) -> Option<CountedRun> {
173 let (family, canonical) = family_for(OsStr::new(name))?;
174 Some(match family {
175 Family::Bzip2 if canonical == "bzip2recover" => {
176 let (code, err) = bzip2recover::run_in(dir, name.as_bytes(), &args);
177 (code, Vec::new(), err, BTreeMap::new())
178 }
179 Family::Bzip2 => {
180 let (code, out, err) = bzip2::run_in(dir, canonical, &args, stdin);
181 (code, out, err, BTreeMap::new())
182 }
183 Family::Gzip => gzip::run_in_counted(gzip_flavour, dir, canonical, &args, stdin, &|_| None),
184 Family::Xz => xz::run_in_counted(dir, canonical, &args, stdin, &|_| None),
185 Family::Zstd => zstd::run_in_counted(dir, canonical, &args, stdin, &|_| None),
186 Family::Lz4 => lz4::run_in_counted(dir, canonical, &args, stdin, &|_| None),
187 Family::Unzip => {
188 unzip::run_in_counted(unzip_flavour, dir, canonical, &args, stdin, &|_| None)
189 }
190 })
191}
192
193pub(crate) fn stdin_is_tty() -> bool {
195 io::stdin().is_terminal()
196}
197
198pub(crate) fn stdout_is_tty() -> bool {
200 io::stdout().is_terminal()
201}
202
203pub(crate) fn copy_metadata_from(meta: &std::fs::Metadata, to: &Path) -> io::Result<()> {
212 copy_metadata_with_mtime(meta, to, None)
213}
214
215pub(crate) fn copy_metadata_with_mtime(
218 meta: &std::fs::Metadata,
219 to: &Path,
220 mtime: Option<std::time::SystemTime>,
221) -> io::Result<()> {
222 let mut times = std::fs::FileTimes::new();
223 if let Ok(t) = meta.accessed() {
224 times = times.set_accessed(t);
225 }
226 if let Some(t) = mtime.or_else(|| meta.modified().ok()) {
227 times = times.set_modified(t);
228 }
229 std::fs::File::options()
230 .write(true)
231 .open(to)?
232 .set_times(times)?;
233 #[cfg(unix)]
234 {
235 use std::os::unix::fs::MetadataExt;
236 match std::os::unix::fs::chown(to, Some(meta.uid()), Some(meta.gid())) {
237 Err(e) if e.kind() != io::ErrorKind::PermissionDenied => return Err(e),
238 _ => {}
239 }
240 }
241 std::fs::set_permissions(to, meta.permissions())
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 const G: gzip::Flavour = gzip::Flavour::Gnu;
251
252 const U: unzip::Flavour = unzip::Flavour::Apple;
255
256 #[test]
259 fn flavour_var_rejects_other_values() {
260 let d = unzip::default_flavour();
261 assert_eq!(unzip::flavour_from(None), Ok(d));
262 assert_eq!(unzip::flavour_from(Some("".into())), Ok(d));
263 assert_eq!(
264 unzip::flavour_from(Some("apple".into())),
265 Ok(unzip::Flavour::Apple)
266 );
267 assert_eq!(
268 unzip::flavour_from(Some("ubuntu".into())),
269 Ok(unzip::Flavour::Ubuntu)
270 );
271 for bad in ["Apple", "UBUNTU", " apple", "debian", "bsd"] {
272 assert_eq!(unzip::flavour_from(Some(bad.into())), Err(bad.into()));
273 }
274 }
275
276 #[test]
279 fn run_in_counted_reaches_the_unzip_family() {
280 let dir = std::env::temp_dir();
281 let run = |name: &str, f: unzip::Flavour| {
282 let (code, out, err, reads) =
283 run_in_counted(&dir, name, vec!["-h".into()], b"", G, f).unwrap();
284 assert!(reads.is_empty(), "{reads:?}");
285 (code, String::from_utf8(out).unwrap(), err)
286 };
287 let (code, out, _) = run("unzip", unzip::Flavour::Apple);
288 assert_eq!(code, 0);
289 assert!(out.contains("Please report bugs to Apple."), "{out}");
290 let (_, out, _) = run("unzip", unzip::Flavour::Ubuntu);
291 assert!(!out.contains("Apple"), "{out}");
292 let (code, out, _) = run("zipinfo", U);
293 assert_eq!(code, 0);
294 assert!(
295 out.starts_with("ZipInfo 3.00 of 20 April 2009, by stuffr "),
296 "{out}"
297 );
298 let (code, out, _) = run("funzip", U);
301 assert!(unzip::EXIT_CODES_APPLE.contains(&code), "exit {code}");
302 assert!(!out.contains("fUnZip"), "{out}");
303 }
304
305 #[test]
308 fn fuzz_names_match_the_dispatch_table() {
309 assert_eq!(stuffr::testing::COMPAT_NAMES, names());
310 }
311
312 #[test]
313 fn compat_names_are_recognised_by_file_name_only() {
314 use std::ffi::OsStr;
315 assert_eq!(family_for(OsStr::new("bzip2")).map(|f| f.1), Some("bzip2"));
316 assert_eq!(
317 family_for(OsStr::new("/usr/local/bin/bunzip2")).map(|f| f.1),
318 Some("bunzip2")
319 );
320 assert_eq!(family_for(OsStr::new("bzcat")).map(|f| f.1), Some("bzcat"));
321 assert_eq!(
322 family_for(OsStr::new("bzip2recover")).map(|f| f.1),
323 Some("bzip2recover")
324 );
325 assert!(family_for(OsStr::new("stuffr")).is_none());
326 assert!(family_for(OsStr::new("/x/bzip2/stuffr")).is_none());
327 assert!(family_for(OsStr::new("bzip2x")).is_none());
328 #[cfg(windows)]
329 assert_eq!(
330 family_for(OsStr::new("bzip2.exe")).map(|f| f.1),
331 Some("bzip2")
332 );
333 }
334
335 #[test]
336 fn stuffr_itself_does_not_dispatch() {
337 assert!(dispatch(vec!["stuffr".into(), "list".into()]).is_none());
338 }
339
340 #[test]
341 fn dispatch_ignores_an_empty_argv() {
342 assert!(dispatch(Vec::new()).is_none());
343 }
344
345 #[cfg(unix)]
346 #[test]
347 fn dispatch_ignores_a_non_utf8_argv0() {
348 use std::os::unix::ffi::OsStrExt;
349 let bad = OsStr::from_bytes(b"bzip2\xff").to_os_string();
350 assert!(dispatch(vec![bad]).is_none());
351 let bad = OsStr::from_bytes(b"/x/\xffbzip2").to_os_string();
352 assert!(dispatch(vec![bad]).is_none());
353 }
354
355 #[test]
358 fn run_in_works_inside_its_directory() {
359 let dir = std::env::temp_dir().join(format!("stuffr-run-in-{}", std::process::id()));
360 let _ = std::fs::remove_dir_all(&dir);
361 std::fs::create_dir_all(&dir).unwrap();
362 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
363 let (code, out, err) =
364 run_in(&dir, "bzip2", vec!["-k".into(), "a".into()], b"", G).unwrap();
365 assert_eq!(
366 (code, out.len()),
367 (0, 0),
368 "{}",
369 String::from_utf8_lossy(&err)
370 );
371 let packed = std::fs::read(dir.join("a.bz2")).unwrap();
372 assert!(packed.starts_with(b"BZh9"));
373 let (code, out, _) = run_in(&dir, "bzcat", vec![], &packed, G).unwrap();
374 assert_eq!((code, out.as_slice()), (0, &b"hello hello hello\n"[..]));
375 let (code, out, err) = run_in(&dir, "bzip2recover", vec!["a.bz2".into()], b"", G).unwrap();
376 assert_eq!(
377 (code, out.len()),
378 (0, 0),
379 "{}",
380 String::from_utf8_lossy(&err)
381 );
382 assert!(dir.join("rec00001a.bz2").exists());
383 let (code, _, err) = run_in(&dir, "bunzip2", vec![], b"BZh9garbage", G).unwrap();
385 assert_eq!(code, 2, "{}", String::from_utf8_lossy(&err));
386 assert!(run_in(&dir, "stuffr", vec![], b"", G).is_none());
387 let _ = std::fs::remove_dir_all(&dir);
388 }
389
390 #[test]
395 fn run_in_counted_counts_the_gzip_familys_inputs() {
396 let dir = std::env::temp_dir().join(format!("stuffr-run-in-count-{}", std::process::id()));
397 for flavour in [gzip::Flavour::Gnu, gzip::Flavour::Bsd] {
398 let _ = std::fs::remove_dir_all(&dir);
399 std::fs::create_dir_all(&dir).unwrap();
400 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
401 let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
402 let (code, _, err, reads) =
403 run_in_counted(&dir, "gzip", args(&["-k", "a"]), b"", flavour, U).unwrap();
404 assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
405 let a = reads[&dir.join("a")];
406 assert_eq!((a.opens, a.bytes, a.len), (1, 18, 18), "{flavour:?}");
407 assert_eq!(reads.len(), 1, "{reads:?}");
408
409 let len = std::fs::metadata(dir.join("a.gz")).unwrap().len();
410 for mode in [&["-t", "a.gz", "a.gz"][..], &["-l", "a.gz"]] {
411 let (code, _, err, reads) =
412 run_in_counted(&dir, "gzip", args(mode), b"", flavour, U).unwrap();
413 assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
414 let gz = reads[&dir.join("a.gz")];
415 assert_eq!(gz.opens, mode.len() as u64 - 1, "{flavour:?} {mode:?}");
416 assert_eq!(gz.len, len);
417 assert!(gz.bytes > 0 && gz.bytes <= 2 * len * gz.opens, "{gz:?}");
418 }
419 let (_, _, _, reads) =
420 run_in_counted(&dir, "bzip2", args(&["-k", "a"]), b"", flavour, U).unwrap();
421 assert!(reads.is_empty(), "{reads:?}");
422 }
423 let _ = std::fs::remove_dir_all(&dir);
424 }
425
426 #[test]
430 fn run_in_counted_counts_the_zstd_familys_inputs() {
431 let dir = std::env::temp_dir().join(format!("stuffr-run-in-zstd-{}", std::process::id()));
432 let _ = std::fs::remove_dir_all(&dir);
433 std::fs::create_dir_all(&dir).unwrap();
434 let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
435 let run = |a: &[&str]| {
436 let (code, _, err, reads) =
437 run_in_counted(&dir, "zstd", args(a), b"", gzip::Flavour::Gnu, U).unwrap();
438 assert_eq!(code, 0, "{a:?}: {}", String::from_utf8_lossy(&err));
439 reads
440 };
441 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
442 std::fs::write(dir.join("d.dict"), b"a raw dictionary\n").unwrap();
443 std::fs::write(dir.join("l"), b"a\n").unwrap();
444
445 let reads = run(&["-q", "-k", "a"]);
446 assert_eq!(
447 reads[&dir.join("a")],
448 InputReads {
449 opens: 1,
450 bytes: 18,
451 len: 18
452 }
453 );
454 let zst = std::fs::metadata(dir.join("a.zst")).unwrap().len();
455
456 let reads = run(&["-d", "-c", "-D", "d.dict", "a.zst"]);
457 let dict = reads[&dir.join("d.dict")];
458 assert_eq!(
459 dict,
460 InputReads {
461 opens: 1,
462 bytes: 17,
463 len: 17
464 }
465 );
466 assert_eq!(reads[&dir.join("a.zst")].len, zst);
467
468 let reads = run(&["-c", "--filelist=l"]);
469 assert_eq!(
470 reads[&dir.join("l")],
471 InputReads {
472 opens: 1,
473 bytes: 2,
474 len: 2
475 }
476 );
477 assert_eq!(reads[&dir.join("a")].opens, 1);
478 let _ = std::fs::remove_dir_all(&dir);
479 }
480
481 #[test]
483 fn run_in_caps_what_a_run_writes() {
484 let dir = std::env::temp_dir().join(format!("stuffr-run-in-cap-{}", std::process::id()));
485 let _ = std::fs::remove_dir_all(&dir);
486 std::fs::create_dir_all(&dir).unwrap();
487 let big = vec![0u8; 9 * 1024 * 1024];
488 let (code, packed, _) = run_in(&dir, "bzip2", vec![], &big, G).unwrap();
489 assert_eq!(code, 0);
490 let (code, out, err) = run_in(&dir, "bzcat", vec![], &packed, G).unwrap();
491 let err = String::from_utf8_lossy(&err);
492 assert_eq!(code, 1, "{err}");
493 let cap = 8 * 1024 * 1024;
496 assert!(
497 out.len() <= cap && out.len() > cap - 64 * 1024,
498 "{}",
499 out.len()
500 );
501 assert!(err.contains("I/O or other error"), "{err}");
502 let (code, out, err) = run_in(&dir, "bzip2", vec!["-t".into()], &packed, G).unwrap();
504 let err = String::from_utf8_lossy(&err);
505 assert_eq!((code, out.len()), (1, 0), "{err}");
506 assert!(err.contains("I/O or other error"), "{err}");
507 let _ = std::fs::remove_dir_all(&dir);
508 }
509
510 #[test]
511 fn names_lists_the_table() {
512 assert_eq!(
513 names(),
514 [
515 "bzip2",
516 "bunzip2",
517 "bzcat",
518 "bzip2recover",
519 "gzip",
520 "gunzip",
521 "zcat",
522 "gzcat",
523 "xz",
524 "unxz",
525 "xzcat",
526 "lzma",
527 "unlzma",
528 "lzcat",
529 "zstd",
530 "unzstd",
531 "zstdcat",
532 "zstdmt",
533 "lz4",
534 "unlz4",
535 "lz4cat",
536 "lz4c",
537 "unzip",
538 "zipinfo",
539 "funzip"
540 ]
541 );
542 }
543
544 #[test]
545 fn copy_metadata_carries_mtime_and_permissions() {
546 let dir = std::env::temp_dir().join(format!("stuffr-meta-{}", std::process::id()));
547 std::fs::create_dir_all(&dir).unwrap();
548 let (a, b) = (dir.join("a"), dir.join("b"));
549 std::fs::write(&a, b"x").unwrap();
550 std::fs::write(&b, b"y").unwrap();
551 let old = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
552 std::fs::File::options()
553 .write(true)
554 .open(&a)
555 .unwrap()
556 .set_modified(old)
557 .unwrap();
558 #[cfg(unix)]
559 {
560 use std::os::unix::fs::PermissionsExt;
561 std::fs::set_permissions(&a, std::fs::Permissions::from_mode(0o640)).unwrap();
562 }
563 copy_metadata_from(&std::fs::metadata(&a).unwrap(), &b).unwrap();
564 let (ma, mb) = (
565 std::fs::metadata(&a).unwrap(),
566 std::fs::metadata(&b).unwrap(),
567 );
568 assert_eq!(ma.modified().unwrap(), mb.modified().unwrap());
569 assert_eq!(ma.permissions(), mb.permissions());
570 let _ = std::fs::remove_dir_all(&dir);
571 }
572}