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<()> {
270 copy_metadata_with_mtime(meta, to, None)
271}
272
273pub(crate) fn copy_metadata_with_mtime(
276 meta: &std::fs::Metadata,
277 to: &Path,
278 mtime: Option<std::time::SystemTime>,
279) -> io::Result<()> {
280 let mut times = std::fs::FileTimes::new();
281 if let Ok(t) = meta.accessed() {
282 times = times.set_accessed(t);
283 }
284 if let Some(t) = mtime.or_else(|| meta.modified().ok()) {
285 times = times.set_modified(t);
286 }
287 let f = host::open_nofollow_for_write(to)?;
291 f.set_times(times)?;
292 #[cfg(unix)]
293 {
294 use std::os::unix::fs::MetadataExt;
295 match std::os::unix::fs::fchown(&f, Some(meta.uid()), Some(meta.gid())) {
296 Err(e) if e.kind() != io::ErrorKind::PermissionDenied => return Err(e),
297 _ => {}
298 }
299 }
300 f.set_permissions(meta.permissions())
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 const G: gzip::Flavour = gzip::Flavour::Gnu;
310
311 const U: unzip::Flavour = unzip::Flavour::Apple;
314
315 const C: compress::Flavour = compress::Flavour::Apple;
318
319 const Z: zip::Flavour = zip::Flavour::Apple;
321
322 #[test]
325 fn flavour_var_rejects_other_values() {
326 let d = unzip::default_flavour();
327 assert_eq!(unzip::flavour_from(None), Ok(d));
328 assert_eq!(unzip::flavour_from(Some("".into())), Ok(d));
329 assert_eq!(
330 unzip::flavour_from(Some("apple".into())),
331 Ok(unzip::Flavour::Apple)
332 );
333 assert_eq!(
334 unzip::flavour_from(Some("ubuntu".into())),
335 Ok(unzip::Flavour::Ubuntu)
336 );
337 for bad in ["Apple", "UBUNTU", " apple", "debian", "bsd"] {
338 assert_eq!(unzip::flavour_from(Some(bad.into())), Err(bad.into()));
339 }
340 }
341
342 #[test]
345 fn run_in_counted_reaches_the_unzip_family() {
346 let dir = std::env::temp_dir();
347 let run = |name: &str, f: unzip::Flavour| {
348 let (code, out, err, reads) =
349 run_in_counted(&dir, name, vec!["-h".into()], b"", G, f, C, Z).unwrap();
350 assert!(reads.is_empty(), "{reads:?}");
351 (code, String::from_utf8(out).unwrap(), err)
352 };
353 let (code, out, _) = run("unzip", unzip::Flavour::Apple);
354 assert_eq!(code, 0);
355 assert!(out.contains("Please report bugs to Apple."), "{out}");
356 let (_, out, _) = run("unzip", unzip::Flavour::Ubuntu);
357 assert!(!out.contains("Apple"), "{out}");
358 let (code, out, _) = run("zipinfo", U);
359 assert_eq!(code, 0);
360 assert!(
361 out.starts_with("ZipInfo 3.00 of 20 April 2009, by stuffr "),
362 "{out}"
363 );
364 let (code, out, _) = run("funzip", U);
367 assert!(unzip::EXIT_CODES_APPLE.contains(&code), "exit {code}");
368 assert!(!out.contains("fUnZip"), "{out}");
369 }
370
371 #[test]
374 fn run_in_counted_reaches_the_compress_family() {
375 struct Guard(std::path::PathBuf);
377 impl Drop for Guard {
378 fn drop(&mut self) {
379 let _ = std::fs::remove_dir_all(&self.0);
380 }
381 }
382 let dir = std::env::temp_dir().join(format!("stuffr-run-in-z-{}", std::process::id()));
383 let _ = std::fs::remove_dir_all(&dir);
384 std::fs::create_dir_all(&dir).unwrap();
385 let _guard = Guard(dir.clone());
386 std::fs::write(dir.join("a"), b"hello hello hello hello\n").unwrap();
387 for (c, usage) in [
388 (
389 compress::Flavour::Apple,
390 "uncompress: illegal option -- x\nusage: uncompress [-cfv] [-b bits] [file ...]\n",
391 ),
392 (
393 compress::Flavour::Ncompress,
394 "Unknown flag: 'x'; Usage: uncompress [-dfhvcVr] [-b maxbits] [--] [path ...]\n",
395 ),
396 ] {
397 let (code, _, err, _) =
398 run_in_counted(&dir, "uncompress", vec!["-x".into()], b"", G, U, c, Z).unwrap();
399 assert_eq!(code, 1, "{c:?}");
400 assert!(
401 String::from_utf8_lossy(&err).starts_with(usage),
402 "{c:?}: {}",
403 String::from_utf8_lossy(&err)
404 );
405 let (code, out, err, reads) = run_in_counted(
406 &dir,
407 "compress",
408 vec!["-c".into(), "a".into()],
409 b"",
410 G,
411 U,
412 c,
413 Z,
414 )
415 .unwrap();
416 assert_eq!(code, 0, "{c:?}: {}", String::from_utf8_lossy(&err));
417 assert!(out.starts_with(&[0x1f, 0x9d, 0x90]), "{c:?}");
418 assert_eq!(reads[&dir.join("a")].opens, 1, "{c:?}");
419 }
420 }
421
422 #[test]
425 fn run_in_counted_reaches_the_zip_family() {
426 let dir = std::env::temp_dir();
427 for name in ["zip", "zipcloak", "zipnote", "zipsplit"] {
428 for (f, origin) in [
429 (zip::Flavour::Apple, "with modifications by Apple Inc."),
430 (zip::Flavour::Ubuntu, "Debian's build"),
431 ] {
432 let (code, out, err, reads) =
433 run_in_counted(&dir, name, vec!["-h".into()], b"", G, U, C, f).unwrap();
434 let out = String::from_utf8(out).unwrap();
435 assert_eq!((code, err.as_slice()), (0, &b""[..]), "{name}");
436 assert!(reads.is_empty(), "{reads:?}");
437 assert!(
438 out.contains(", by stuffr ") && out.contains(origin),
439 "{name} {f:?}: {out}"
440 );
441 }
442 }
443 let (code, out, _) = run_in(&dir, "zipsplit", vec!["-L".into()], b"", G).unwrap();
445 assert_eq!(code, 0);
446 assert!(String::from_utf8(out).unwrap().contains("Info-ZIP"));
447 }
448
449 #[test]
452 fn fuzz_names_match_the_dispatch_table() {
453 assert_eq!(stuffr::testing::COMPAT_NAMES, names());
454 }
455
456 #[test]
457 fn compat_names_are_recognised_by_file_name_only() {
458 use std::ffi::OsStr;
459 assert_eq!(family_for(OsStr::new("bzip2")).map(|f| f.1), Some("bzip2"));
460 assert_eq!(
461 family_for(OsStr::new("/usr/local/bin/bunzip2")).map(|f| f.1),
462 Some("bunzip2")
463 );
464 assert_eq!(family_for(OsStr::new("bzcat")).map(|f| f.1), Some("bzcat"));
465 assert_eq!(
466 family_for(OsStr::new("bzip2recover")).map(|f| f.1),
467 Some("bzip2recover")
468 );
469 assert!(family_for(OsStr::new("stuffr")).is_none());
470 assert!(family_for(OsStr::new("/x/bzip2/stuffr")).is_none());
471 assert!(family_for(OsStr::new("bzip2x")).is_none());
472 #[cfg(windows)]
473 assert_eq!(
474 family_for(OsStr::new("bzip2.exe")).map(|f| f.1),
475 Some("bzip2")
476 );
477 }
478
479 #[test]
480 fn stuffr_itself_does_not_dispatch() {
481 assert!(dispatch(vec!["stuffr".into(), "list".into()]).is_none());
482 }
483
484 #[test]
485 fn dispatch_ignores_an_empty_argv() {
486 assert!(dispatch(Vec::new()).is_none());
487 }
488
489 #[cfg(unix)]
490 #[test]
491 fn dispatch_ignores_a_non_utf8_argv0() {
492 use std::os::unix::ffi::OsStrExt;
493 let bad = OsStr::from_bytes(b"bzip2\xff").to_os_string();
494 assert!(dispatch(vec![bad]).is_none());
495 let bad = OsStr::from_bytes(b"/x/\xffbzip2").to_os_string();
496 assert!(dispatch(vec![bad]).is_none());
497 }
498
499 #[test]
502 fn run_in_works_inside_its_directory() {
503 let dir = std::env::temp_dir().join(format!("stuffr-run-in-{}", std::process::id()));
504 let _ = std::fs::remove_dir_all(&dir);
505 std::fs::create_dir_all(&dir).unwrap();
506 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
507 let (code, out, err) =
508 run_in(&dir, "bzip2", vec!["-k".into(), "a".into()], b"", G).unwrap();
509 assert_eq!(
510 (code, out.len()),
511 (0, 0),
512 "{}",
513 String::from_utf8_lossy(&err)
514 );
515 let packed = std::fs::read(dir.join("a.bz2")).unwrap();
516 assert!(packed.starts_with(b"BZh9"));
517 let (code, out, _) = run_in(&dir, "bzcat", vec![], &packed, G).unwrap();
518 assert_eq!((code, out.as_slice()), (0, &b"hello hello hello\n"[..]));
519 let (code, out, err) = run_in(&dir, "bzip2recover", vec!["a.bz2".into()], b"", G).unwrap();
520 assert_eq!(
521 (code, out.len()),
522 (0, 0),
523 "{}",
524 String::from_utf8_lossy(&err)
525 );
526 assert!(dir.join("rec00001a.bz2").exists());
527 let (code, _, err) = run_in(&dir, "bunzip2", vec![], b"BZh9garbage", G).unwrap();
529 assert_eq!(code, 2, "{}", String::from_utf8_lossy(&err));
530 assert!(run_in(&dir, "stuffr", vec![], b"", G).is_none());
531 let _ = std::fs::remove_dir_all(&dir);
532 }
533
534 #[test]
539 fn run_in_counted_counts_the_gzip_familys_inputs() {
540 let dir = std::env::temp_dir().join(format!("stuffr-run-in-count-{}", std::process::id()));
541 for flavour in [gzip::Flavour::Gnu, gzip::Flavour::Bsd] {
542 let _ = std::fs::remove_dir_all(&dir);
543 std::fs::create_dir_all(&dir).unwrap();
544 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
545 let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
546 let (code, _, err, reads) =
547 run_in_counted(&dir, "gzip", args(&["-k", "a"]), b"", flavour, U, C, Z).unwrap();
548 assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
549 let a = reads[&dir.join("a")];
550 assert_eq!((a.opens, a.bytes, a.len), (1, 18, 18), "{flavour:?}");
551 assert_eq!(reads.len(), 1, "{reads:?}");
552
553 let len = std::fs::metadata(dir.join("a.gz")).unwrap().len();
554 for mode in [&["-t", "a.gz", "a.gz"][..], &["-l", "a.gz"]] {
555 let (code, _, err, reads) =
556 run_in_counted(&dir, "gzip", args(mode), b"", flavour, U, C, Z).unwrap();
557 assert_eq!(code, 0, "{}", String::from_utf8_lossy(&err));
558 let gz = reads[&dir.join("a.gz")];
559 assert_eq!(gz.opens, mode.len() as u64 - 1, "{flavour:?} {mode:?}");
560 assert_eq!(gz.len, len);
561 assert!(gz.bytes > 0 && gz.bytes <= 2 * len * gz.opens, "{gz:?}");
562 }
563 let (_, _, _, reads) =
564 run_in_counted(&dir, "bzip2", args(&["-k", "a"]), b"", flavour, U, C, Z).unwrap();
565 assert!(reads.is_empty(), "{reads:?}");
566 }
567 let _ = std::fs::remove_dir_all(&dir);
568 }
569
570 #[test]
574 fn run_in_counted_counts_the_zstd_familys_inputs() {
575 let dir = std::env::temp_dir().join(format!("stuffr-run-in-zstd-{}", std::process::id()));
576 let _ = std::fs::remove_dir_all(&dir);
577 std::fs::create_dir_all(&dir).unwrap();
578 let args = |a: &[&str]| a.iter().map(OsString::from).collect::<Vec<_>>();
579 let run = |a: &[&str]| {
580 let (code, _, err, reads) =
581 run_in_counted(&dir, "zstd", args(a), b"", gzip::Flavour::Gnu, U, C, Z).unwrap();
582 assert_eq!(code, 0, "{a:?}: {}", String::from_utf8_lossy(&err));
583 reads
584 };
585 std::fs::write(dir.join("a"), b"hello hello hello\n").unwrap();
586 std::fs::write(dir.join("d.dict"), b"a raw dictionary\n").unwrap();
587 std::fs::write(dir.join("l"), b"a\n").unwrap();
588
589 let reads = run(&["-q", "-k", "a"]);
590 assert_eq!(
591 reads[&dir.join("a")],
592 InputReads {
593 opens: 1,
594 bytes: 18,
595 len: 18,
596 ..Default::default()
597 }
598 );
599 let zst = std::fs::metadata(dir.join("a.zst")).unwrap().len();
600
601 let reads = run(&["-d", "-c", "-D", "d.dict", "a.zst"]);
602 let dict = reads[&dir.join("d.dict")];
603 assert_eq!(
604 dict,
605 InputReads {
606 opens: 1,
607 bytes: 17,
608 len: 17,
609 ..Default::default()
610 }
611 );
612 assert_eq!(reads[&dir.join("a.zst")].len, zst);
613
614 let reads = run(&["-c", "--filelist=l"]);
615 assert_eq!(
616 reads[&dir.join("l")],
617 InputReads {
618 opens: 1,
619 bytes: 2,
620 len: 2,
621 ..Default::default()
622 }
623 );
624 assert_eq!(reads[&dir.join("a")].opens, 1);
625 let _ = std::fs::remove_dir_all(&dir);
626 }
627
628 #[test]
630 fn run_in_caps_what_a_run_writes() {
631 let dir = std::env::temp_dir().join(format!("stuffr-run-in-cap-{}", std::process::id()));
632 let _ = std::fs::remove_dir_all(&dir);
633 std::fs::create_dir_all(&dir).unwrap();
634 let big = vec![0u8; 9 * 1024 * 1024];
635 let (code, packed, _) = run_in(&dir, "bzip2", vec![], &big, G).unwrap();
636 assert_eq!(code, 0);
637 let (code, out, err) = run_in(&dir, "bzcat", vec![], &packed, G).unwrap();
638 let err = String::from_utf8_lossy(&err);
639 assert_eq!(code, 1, "{err}");
640 let cap = 8 * 1024 * 1024;
643 assert!(
644 out.len() <= cap && out.len() > cap - 64 * 1024,
645 "{}",
646 out.len()
647 );
648 assert!(err.contains("I/O or other error"), "{err}");
649 let (code, out, err) = run_in(&dir, "bzip2", vec!["-t".into()], &packed, G).unwrap();
651 let err = String::from_utf8_lossy(&err);
652 assert_eq!((code, out.len()), (1, 0), "{err}");
653 assert!(err.contains("I/O or other error"), "{err}");
654 let _ = std::fs::remove_dir_all(&dir);
655 }
656
657 #[test]
658 fn names_has_thirty_four_entries() {
659 assert_eq!(names().len(), 34);
660 }
661
662 #[test]
663 fn names_lists_the_table() {
664 assert_eq!(
665 names(),
666 [
667 "bzip2",
668 "bunzip2",
669 "bzcat",
670 "bzip2recover",
671 "gzip",
672 "gunzip",
673 "zcat",
674 "gzcat",
675 "xz",
676 "unxz",
677 "xzcat",
678 "lzma",
679 "unlzma",
680 "lzcat",
681 "zstd",
682 "unzstd",
683 "zstdcat",
684 "zstdmt",
685 "lz4",
686 "unlz4",
687 "lz4cat",
688 "lz4c",
689 "unzip",
690 "zipinfo",
691 "funzip",
692 "compress",
693 "uncompress",
694 "zip",
695 "zipcloak",
696 "zipnote",
697 "zipsplit",
698 "brotli",
699 "unbrotli",
700 "brcat"
701 ]
702 );
703 }
704
705 #[test]
706 fn copy_metadata_carries_mtime_and_permissions() {
707 let dir = std::env::temp_dir().join(format!("stuffr-meta-{}", std::process::id()));
708 std::fs::create_dir_all(&dir).unwrap();
709 let (a, b) = (dir.join("a"), dir.join("b"));
710 std::fs::write(&a, b"x").unwrap();
711 std::fs::write(&b, b"y").unwrap();
712 let old = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000_000);
713 std::fs::File::options()
714 .write(true)
715 .open(&a)
716 .unwrap()
717 .set_modified(old)
718 .unwrap();
719 #[cfg(unix)]
720 {
721 use std::os::unix::fs::PermissionsExt;
722 std::fs::set_permissions(&a, std::fs::Permissions::from_mode(0o640)).unwrap();
723 }
724 copy_metadata_from(&std::fs::metadata(&a).unwrap(), &b).unwrap();
725 let (ma, mb) = (
726 std::fs::metadata(&a).unwrap(),
727 std::fs::metadata(&b).unwrap(),
728 );
729 assert_eq!(ma.modified().unwrap(), mb.modified().unwrap());
730 assert_eq!(ma.permissions(), mb.permissions());
731 let _ = std::fs::remove_dir_all(&dir);
732 }
733}