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 brotli;
12pub mod bzip2;
13pub mod bzip2recover;
14pub mod compress;
15pub mod gzip;
16mod host;
17pub mod links;
18pub mod lz4;
19pub mod unzip;
20pub mod xz;
21pub mod zip;
22pub mod zstd;
23
24#[cfg(test)]
25pub(crate) use host::ARM_TEST_LOCK;
26#[doc(hidden)]
27pub use host::InputReads;
28#[cfg(unix)]
31pub(crate) use host::hook_run_signals;
32pub(crate) use host::{CleanupGuard, create_armed, with_run_signals_held};
34#[cfg(all(test, unix))]
35pub(crate) use host::{armed_path, disarm_inherited};
36
37#[doc(hidden)]
40pub type CountedRun = (i32, Vec<u8>, Vec<u8>, BTreeMap<PathBuf, InputReads>);
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44#[non_exhaustive]
45pub enum Family {
46 Bzip2,
48 Gzip,
51 Xz,
53 Zstd,
55 Lz4,
57 Brotli,
59 Unzip,
62 Compress,
65 Zip,
68}
69
70const NAMES: &[(&str, Family)] = &[
73 ("bzip2", Family::Bzip2),
74 ("bunzip2", Family::Bzip2),
75 ("bzcat", Family::Bzip2),
76 ("bzip2recover", Family::Bzip2),
77 ("gzip", Family::Gzip),
78 ("gunzip", Family::Gzip),
79 ("zcat", Family::Gzip),
80 ("gzcat", Family::Gzip),
81 ("xz", Family::Xz),
82 ("unxz", Family::Xz),
83 ("xzcat", Family::Xz),
84 ("lzma", Family::Xz),
85 ("unlzma", Family::Xz),
86 ("lzcat", Family::Xz),
87 ("zstd", Family::Zstd),
88 ("unzstd", Family::Zstd),
89 ("zstdcat", Family::Zstd),
90 ("zstdmt", Family::Zstd),
91 ("lz4", Family::Lz4),
92 ("unlz4", Family::Lz4),
93 ("lz4cat", Family::Lz4),
94 ("lz4c", Family::Lz4),
95 ("unzip", Family::Unzip),
96 ("zipinfo", Family::Unzip),
97 ("funzip", Family::Unzip),
98 ("compress", Family::Compress),
99 ("uncompress", Family::Compress),
100 ("zip", Family::Zip),
101 ("zipcloak", Family::Zip),
102 ("zipnote", Family::Zip),
103 ("zipsplit", Family::Zip),
104 ("brotli", Family::Brotli),
105 ("unbrotli", Family::Brotli),
106 ("brcat", Family::Brotli),
107];
108
109pub fn names() -> &'static [&'static str] {
111 static LIST: std::sync::OnceLock<Vec<&'static str>> = std::sync::OnceLock::new();
112 LIST.get_or_init(|| NAMES.iter().map(|&(n, _)| n).collect())
113}
114
115pub fn family_for(argv0: &OsStr) -> Option<(Family, &'static str)> {
119 let file = Path::new(argv0).file_name()?;
120 let file = file.to_str()?;
121 #[cfg(windows)]
122 let file = file.strip_suffix(".exe").unwrap_or(file);
123 NAMES
124 .iter()
125 .find(|(n, _)| *n == file)
126 .map(|&(n, fam)| (fam, n))
127}
128
129pub fn dispatch(args: Vec<OsString>) -> Option<ExitCode> {
134 #[cfg(all(unix, debug_assertions))]
137 if Path::new(args.first()?).file_name() == Some(OsStr::new("__compat-signal-test")) {
138 return Some(host::signal_test(&args[1..]));
139 }
140 let (family, name) = family_for(args.first()?)?;
141 let mut args = args.into_iter();
142 let argv0 = args.next().unwrap_or_default();
143 let rest = args.collect();
144 Some(match family {
145 Family::Bzip2 if name == "bzip2recover" => bzip2recover::run(argv0, rest),
147 Family::Bzip2 => bzip2::run(name, rest),
148 Family::Gzip => gzip::run(name, rest),
149 Family::Xz => xz::run(name, &argv0, rest),
150 Family::Zstd => zstd::run(name, &argv0, rest),
151 Family::Lz4 => lz4::run(name, &argv0, rest),
152 Family::Brotli => brotli::run(name, &argv0, rest),
153 Family::Unzip => unzip::run(name, &argv0, rest),
154 Family::Compress => compress::run(name, rest),
155 Family::Zip => zip::run(name, &argv0, rest),
156 })
157}
158
159#[doc(hidden)]
172pub fn run_in(
173 dir: &Path,
174 name: &str,
175 args: Vec<OsString>,
176 stdin: &[u8],
177 gzip_flavour: gzip::Flavour,
178) -> Option<(i32, Vec<u8>, Vec<u8>)> {
179 run_in_counted(
180 dir,
181 name,
182 args,
183 stdin,
184 gzip_flavour,
185 unzip::default_flavour(),
186 compress::default_flavour(),
187 zip::default_flavour(),
188 )
189 .map(|(code, out, err, _)| (code, out, err))
190}
191
192#[doc(hidden)]
204#[allow(clippy::too_many_arguments)]
205pub fn run_in_counted(
206 dir: &Path,
207 name: &str,
208 args: Vec<OsString>,
209 stdin: &[u8],
210 gzip_flavour: gzip::Flavour,
211 unzip_flavour: unzip::Flavour,
212 compress_flavour: compress::Flavour,
213 zip_flavour: zip::Flavour,
214) -> Option<CountedRun> {
215 let (family, canonical) = family_for(OsStr::new(name))?;
216 Some(match family {
217 Family::Bzip2 if canonical == "bzip2recover" => {
218 let (code, err) = bzip2recover::run_in(dir, name.as_bytes(), &args);
219 (code, Vec::new(), err, BTreeMap::new())
220 }
221 Family::Bzip2 => {
222 let (code, out, err) = bzip2::run_in(dir, canonical, &args, stdin);
223 (code, out, err, BTreeMap::new())
224 }
225 Family::Gzip => gzip::run_in_counted(gzip_flavour, dir, canonical, &args, stdin, &|_| None),
226 Family::Xz => xz::run_in_counted(dir, canonical, &args, stdin, &|_| None),
227 Family::Zstd => zstd::run_in_counted(dir, canonical, &args, stdin, &|_| None),
228 Family::Lz4 => lz4::run_in_counted(dir, canonical, &args, stdin, &|_| None),
229 Family::Brotli => brotli::run_in_counted(dir, canonical, &args, stdin),
230 Family::Unzip => {
231 unzip::run_in_counted(unzip_flavour, dir, canonical, &args, stdin, &|_| None)
232 }
233 Family::Compress => {
234 compress::run_in_counted(compress_flavour, dir, canonical, &args, stdin, &|_| None)
235 }
236 Family::Zip => zip::run_in_counted(zip_flavour, dir, canonical, &args, stdin),
237 })
238}
239
240#[doc(hidden)]
246pub fn sandbox_name(b: &[u8]) -> Vec<u8> {
247 host::confine_name(b)
248}
249
250pub(crate) fn stdin_is_tty() -> bool {
252 io::stdin().is_terminal()
253}
254
255pub(crate) fn stdout_is_tty() -> bool {
257 io::stdout().is_terminal()
258}
259
260pub(crate) fn copy_metadata_from(meta: &std::fs::Metadata, to: &Path) -> io::Result<()> {
269 copy_metadata_with_mtime(meta, to, None)
270}
271
272pub(crate) fn copy_metadata_with_mtime(
275 meta: &std::fs::Metadata,
276 to: &Path,
277 mtime: Option<std::time::SystemTime>,
278) -> io::Result<()> {
279 let mut times = std::fs::FileTimes::new();
280 if let Ok(t) = meta.accessed() {
281 times = times.set_accessed(t);
282 }
283 if let Some(t) = mtime.or_else(|| meta.modified().ok()) {
284 times = times.set_modified(t);
285 }
286 std::fs::File::options()
287 .write(true)
288 .open(to)?
289 .set_times(times)?;
290 #[cfg(unix)]
291 {
292 use std::os::unix::fs::MetadataExt;
293 match std::os::unix::fs::chown(to, Some(meta.uid()), Some(meta.gid())) {
294 Err(e) if e.kind() != io::ErrorKind::PermissionDenied => return Err(e),
295 _ => {}
296 }
297 }
298 std::fs::set_permissions(to, meta.permissions())
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 const G: gzip::Flavour = gzip::Flavour::Gnu;
308
309 const U: unzip::Flavour = unzip::Flavour::Apple;
312
313 const C: compress::Flavour = compress::Flavour::Apple;
316
317 const Z: zip::Flavour = zip::Flavour::Apple;
319
320 #[test]
323 fn flavour_var_rejects_other_values() {
324 let d = unzip::default_flavour();
325 assert_eq!(unzip::flavour_from(None), Ok(d));
326 assert_eq!(unzip::flavour_from(Some("".into())), Ok(d));
327 assert_eq!(
328 unzip::flavour_from(Some("apple".into())),
329 Ok(unzip::Flavour::Apple)
330 );
331 assert_eq!(
332 unzip::flavour_from(Some("ubuntu".into())),
333 Ok(unzip::Flavour::Ubuntu)
334 );
335 for bad in ["Apple", "UBUNTU", " apple", "debian", "bsd"] {
336 assert_eq!(unzip::flavour_from(Some(bad.into())), Err(bad.into()));
337 }
338 }
339
340 #[test]
343 fn run_in_counted_reaches_the_unzip_family() {
344 let dir = std::env::temp_dir();
345 let run = |name: &str, f: unzip::Flavour| {
346 let (code, out, err, reads) =
347 run_in_counted(&dir, name, vec!["-h".into()], b"", G, f, C, Z).unwrap();
348 assert!(reads.is_empty(), "{reads:?}");
349 (code, String::from_utf8(out).unwrap(), err)
350 };
351 let (code, out, _) = run("unzip", unzip::Flavour::Apple);
352 assert_eq!(code, 0);
353 assert!(out.contains("Please report bugs to Apple."), "{out}");
354 let (_, out, _) = run("unzip", unzip::Flavour::Ubuntu);
355 assert!(!out.contains("Apple"), "{out}");
356 let (code, out, _) = run("zipinfo", U);
357 assert_eq!(code, 0);
358 assert!(
359 out.starts_with("ZipInfo 3.00 of 20 April 2009, by stuffr "),
360 "{out}"
361 );
362 let (code, out, _) = run("funzip", U);
365 assert!(unzip::EXIT_CODES_APPLE.contains(&code), "exit {code}");
366 assert!(!out.contains("fUnZip"), "{out}");
367 }
368
369 #[test]
372 fn run_in_counted_reaches_the_compress_family() {
373 struct Guard(std::path::PathBuf);
375 impl Drop for Guard {
376 fn drop(&mut self) {
377 let _ = std::fs::remove_dir_all(&self.0);
378 }
379 }
380 let dir = std::env::temp_dir().join(format!("stuffr-run-in-z-{}", std::process::id()));
381 let _ = std::fs::remove_dir_all(&dir);
382 std::fs::create_dir_all(&dir).unwrap();
383 let _guard = Guard(dir.clone());
384 std::fs::write(dir.join("a"), b"hello hello hello hello\n").unwrap();
385 for (c, usage) in [
386 (
387 compress::Flavour::Apple,
388 "uncompress: illegal option -- x\nusage: uncompress [-cfv] [-b bits] [file ...]\n",
389 ),
390 (
391 compress::Flavour::Ncompress,
392 "Unknown flag: 'x'; Usage: uncompress [-dfhvcVr] [-b maxbits] [--] [path ...]\n",
393 ),
394 ] {
395 let (code, _, err, _) =
396 run_in_counted(&dir, "uncompress", vec!["-x".into()], b"", G, U, c, Z).unwrap();
397 assert_eq!(code, 1, "{c:?}");
398 assert!(
399 String::from_utf8_lossy(&err).starts_with(usage),
400 "{c:?}: {}",
401 String::from_utf8_lossy(&err)
402 );
403 let (code, out, err, reads) = run_in_counted(
404 &dir,
405 "compress",
406 vec!["-c".into(), "a".into()],
407 b"",
408 G,
409 U,
410 c,
411 Z,
412 )
413 .unwrap();
414 assert_eq!(code, 0, "{c:?}: {}", String::from_utf8_lossy(&err));
415 assert!(out.starts_with(&[0x1f, 0x9d, 0x90]), "{c:?}");
416 assert_eq!(reads[&dir.join("a")].opens, 1, "{c:?}");
417 }
418 }
419
420 #[test]
423 fn run_in_counted_reaches_the_zip_family() {
424 let dir = std::env::temp_dir();
425 for name in ["zip", "zipcloak", "zipnote", "zipsplit"] {
426 for (f, origin) in [
427 (zip::Flavour::Apple, "with modifications by Apple Inc."),
428 (zip::Flavour::Ubuntu, "Debian's build"),
429 ] {
430 let (code, out, err, reads) =
431 run_in_counted(&dir, name, vec!["-h".into()], b"", G, U, C, f).unwrap();
432 let out = String::from_utf8(out).unwrap();
433 assert_eq!((code, err.as_slice()), (0, &b""[..]), "{name}");
434 assert!(reads.is_empty(), "{reads:?}");
435 assert!(
436 out.contains(", by stuffr ") && out.contains(origin),
437 "{name} {f:?}: {out}"
438 );
439 }
440 }
441 let (code, out, _) = run_in(&dir, "zipsplit", vec!["-L".into()], b"", G).unwrap();
443 assert_eq!(code, 0);
444 assert!(String::from_utf8(out).unwrap().contains("Info-ZIP"));
445 }
446
447 #[test]
450 fn fuzz_names_match_the_dispatch_table() {
451 assert_eq!(stuffr::testing::COMPAT_NAMES, names());
452 }
453
454 #[test]
455 fn compat_names_are_recognised_by_file_name_only() {
456 use std::ffi::OsStr;
457 assert_eq!(family_for(OsStr::new("bzip2")).map(|f| f.1), Some("bzip2"));
458 assert_eq!(
459 family_for(OsStr::new("/usr/local/bin/bunzip2")).map(|f| f.1),
460 Some("bunzip2")
461 );
462 assert_eq!(family_for(OsStr::new("bzcat")).map(|f| f.1), Some("bzcat"));
463 assert_eq!(
464 family_for(OsStr::new("bzip2recover")).map(|f| f.1),
465 Some("bzip2recover")
466 );
467 assert!(family_for(OsStr::new("stuffr")).is_none());
468 assert!(family_for(OsStr::new("/x/bzip2/stuffr")).is_none());
469 assert!(family_for(OsStr::new("bzip2x")).is_none());
470 #[cfg(windows)]
471 assert_eq!(
472 family_for(OsStr::new("bzip2.exe")).map(|f| f.1),
473 Some("bzip2")
474 );
475 }
476
477 #[test]
478 fn stuffr_itself_does_not_dispatch() {
479 assert!(dispatch(vec!["stuffr".into(), "list".into()]).is_none());
480 }
481
482 #[test]
483 fn dispatch_ignores_an_empty_argv() {
484 assert!(dispatch(Vec::new()).is_none());
485 }
486
487 #[cfg(unix)]
488 #[test]
489 fn dispatch_ignores_a_non_utf8_argv0() {
490 use std::os::unix::ffi::OsStrExt;
491 let bad = OsStr::from_bytes(b"bzip2\xff").to_os_string();
492 assert!(dispatch(vec![bad]).is_none());
493 let bad = OsStr::from_bytes(b"/x/\xffbzip2").to_os_string();
494 assert!(dispatch(vec![bad]).is_none());
495 }
496
497 #[test]
500 fn run_in_works_inside_its_directory() {
501 let dir = std::env::temp_dir().join(format!("stuffr-run-in-{}", std::process::id()));
502 let _ = std::fs::remove_dir_all(&dir);
503 std::fs::create_dir_all(&dir).unwrap();
504 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
505 let (code, out, err) =
506 run_in(&dir, "bzip2", vec!["-k".into(), "a".into()], b"", G).unwrap();
507 assert_eq!(
508 (code, out.len()),
509 (0, 0),
510 "{}",
511 String::from_utf8_lossy(&err)
512 );
513 let packed = std::fs::read(dir.join("a.bz2")).unwrap();
514 assert!(packed.starts_with(b"BZh9"));
515 let (code, out, _) = run_in(&dir, "bzcat", vec![], &packed, G).unwrap();
516 assert_eq!((code, out.as_slice()), (0, &b"hello hello hello\n"[..]));
517 let (code, out, err) = run_in(&dir, "bzip2recover", vec!["a.bz2".into()], b"", G).unwrap();
518 assert_eq!(
519 (code, out.len()),
520 (0, 0),
521 "{}",
522 String::from_utf8_lossy(&err)
523 );
524 assert!(dir.join("rec00001a.bz2").exists());
525 let (code, _, err) = run_in(&dir, "bunzip2", vec![], b"BZh9garbage", G).unwrap();
527 assert_eq!(code, 2, "{}", String::from_utf8_lossy(&err));
528 assert!(run_in(&dir, "stuffr", vec![], b"", G).is_none());
529 let _ = std::fs::remove_dir_all(&dir);
530 }
531
532 #[test]
537 fn run_in_counted_counts_the_gzip_familys_inputs() {
538 let dir = std::env::temp_dir().join(format!("stuffr-run-in-count-{}", std::process::id()));
539 for flavour in [gzip::Flavour::Gnu, gzip::Flavour::Bsd] {
540 let _ = std::fs::remove_dir_all(&dir);
541 std::fs::create_dir_all(&dir).unwrap();
542 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
543 let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
544 let (code, _, err, reads) =
545 run_in_counted(&dir, "gzip", args(&["-k", "a"]), b"", flavour, U, C, Z).unwrap();
546 assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
547 let a = reads[&dir.join("a")];
548 assert_eq!((a.opens, a.bytes, a.len), (1, 18, 18), "{flavour:?}");
549 assert_eq!(reads.len(), 1, "{reads:?}");
550
551 let len = std::fs::metadata(dir.join("a.gz")).unwrap().len();
552 for mode in [&["-t", "a.gz", "a.gz"][..], &["-l", "a.gz"]] {
553 let (code, _, err, reads) =
554 run_in_counted(&dir, "gzip", args(mode), b"", flavour, U, C, Z).unwrap();
555 assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
556 let gz = reads[&dir.join("a.gz")];
557 assert_eq!(gz.opens, mode.len() as u64 - 1, "{flavour:?} {mode:?}");
558 assert_eq!(gz.len, len);
559 assert!(gz.bytes > 0 && gz.bytes <= 2 * len * gz.opens, "{gz:?}");
560 }
561 let (_, _, _, reads) =
562 run_in_counted(&dir, "bzip2", args(&["-k", "a"]), b"", flavour, U, C, Z).unwrap();
563 assert!(reads.is_empty(), "{reads:?}");
564 }
565 let _ = std::fs::remove_dir_all(&dir);
566 }
567
568 #[test]
572 fn run_in_counted_counts_the_zstd_familys_inputs() {
573 let dir = std::env::temp_dir().join(format!("stuffr-run-in-zstd-{}", std::process::id()));
574 let _ = std::fs::remove_dir_all(&dir);
575 std::fs::create_dir_all(&dir).unwrap();
576 let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
577 let run = |a: &[&str]| {
578 let (code, _, err, reads) =
579 run_in_counted(&dir, "zstd", args(a), b"", gzip::Flavour::Gnu, U, C, Z).unwrap();
580 assert_eq!(code, 0, "{a:?}: {}", String::from_utf8_lossy(&err));
581 reads
582 };
583 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
584 std::fs::write(dir.join("d.dict"), b"a raw dictionary\n").unwrap();
585 std::fs::write(dir.join("l"), b"a\n").unwrap();
586
587 let reads = run(&["-q", "-k", "a"]);
588 assert_eq!(
589 reads[&dir.join("a")],
590 InputReads {
591 opens: 1,
592 bytes: 18,
593 len: 18,
594 ..Default::default()
595 }
596 );
597 let zst = std::fs::metadata(dir.join("a.zst")).unwrap().len();
598
599 let reads = run(&["-d", "-c", "-D", "d.dict", "a.zst"]);
600 let dict = reads[&dir.join("d.dict")];
601 assert_eq!(
602 dict,
603 InputReads {
604 opens: 1,
605 bytes: 17,
606 len: 17,
607 ..Default::default()
608 }
609 );
610 assert_eq!(reads[&dir.join("a.zst")].len, zst);
611
612 let reads = run(&["-c", "--filelist=l"]);
613 assert_eq!(
614 reads[&dir.join("l")],
615 InputReads {
616 opens: 1,
617 bytes: 2,
618 len: 2,
619 ..Default::default()
620 }
621 );
622 assert_eq!(reads[&dir.join("a")].opens, 1);
623 let _ = std::fs::remove_dir_all(&dir);
624 }
625
626 #[test]
628 fn run_in_caps_what_a_run_writes() {
629 let dir = std::env::temp_dir().join(format!("stuffr-run-in-cap-{}", std::process::id()));
630 let _ = std::fs::remove_dir_all(&dir);
631 std::fs::create_dir_all(&dir).unwrap();
632 let big = vec![0u8; 9 * 1024 * 1024];
633 let (code, packed, _) = run_in(&dir, "bzip2", vec![], &big, G).unwrap();
634 assert_eq!(code, 0);
635 let (code, out, err) = run_in(&dir, "bzcat", vec![], &packed, G).unwrap();
636 let err = String::from_utf8_lossy(&err);
637 assert_eq!(code, 1, "{err}");
638 let cap = 8 * 1024 * 1024;
641 assert!(
642 out.len() <= cap && out.len() > cap - 64 * 1024,
643 "{}",
644 out.len()
645 );
646 assert!(err.contains("I/O or other error"), "{err}");
647 let (code, out, err) = run_in(&dir, "bzip2", vec!["-t".into()], &packed, G).unwrap();
649 let err = String::from_utf8_lossy(&err);
650 assert_eq!((code, out.len()), (1, 0), "{err}");
651 assert!(err.contains("I/O or other error"), "{err}");
652 let _ = std::fs::remove_dir_all(&dir);
653 }
654
655 #[test]
656 fn names_has_thirty_four_entries() {
657 assert_eq!(names().len(), 34);
658 }
659
660 #[test]
661 fn names_lists_the_table() {
662 assert_eq!(
663 names(),
664 [
665 "bzip2",
666 "bunzip2",
667 "bzcat",
668 "bzip2recover",
669 "gzip",
670 "gunzip",
671 "zcat",
672 "gzcat",
673 "xz",
674 "unxz",
675 "xzcat",
676 "lzma",
677 "unlzma",
678 "lzcat",
679 "zstd",
680 "unzstd",
681 "zstdcat",
682 "zstdmt",
683 "lz4",
684 "unlz4",
685 "lz4cat",
686 "lz4c",
687 "unzip",
688 "zipinfo",
689 "funzip",
690 "compress",
691 "uncompress",
692 "zip",
693 "zipcloak",
694 "zipnote",
695 "zipsplit",
696 "brotli",
697 "unbrotli",
698 "brcat"
699 ]
700 );
701 }
702
703 #[test]
704 fn copy_metadata_carries_mtime_and_permissions() {
705 let dir = std::env::temp_dir().join(format!("stuffr-meta-{}", std::process::id()));
706 std::fs::create_dir_all(&dir).unwrap();
707 let (a, b) = (dir.join("a"), dir.join("b"));
708 std::fs::write(&a, b"x").unwrap();
709 std::fs::write(&b, b"y").unwrap();
710 let old = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
711 std::fs::File::options()
712 .write(true)
713 .open(&a)
714 .unwrap()
715 .set_modified(old)
716 .unwrap();
717 #[cfg(unix)]
718 {
719 use std::os::unix::fs::PermissionsExt;
720 std::fs::set_permissions(&a, std::fs::Permissions::from_mode(0o640)).unwrap();
721 }
722 copy_metadata_from(&std::fs::metadata(&a).unwrap(), &b).unwrap();
723 let (ma, mb) = (
724 std::fs::metadata(&a).unwrap(),
725 std::fs::metadata(&b).unwrap(),
726 );
727 assert_eq!(ma.modified().unwrap(), mb.modified().unwrap());
728 assert_eq!(ma.permissions(), mb.permissions());
729 let _ = std::fs::remove_dir_all(&dir);
730 }
731}