1use crate::container::{ContainerIndex, EntryMeta};
55use std::path::Path;
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum UnsupportedKind {
64 Mode,
66 Btime,
68 Owner,
70 Xattr,
72 Symlink,
80}
81
82impl UnsupportedKind {
83 #[must_use]
88 pub const fn code(self) -> &'static str {
89 match self {
90 Self::Mode => "mode",
91 Self::Btime => "btime",
92 Self::Owner => "owner",
93 Self::Xattr => "xattr",
94 Self::Symlink => "symlink",
95 }
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct RestoreFailure {
102 pub path: String,
104 pub item: &'static str,
106 pub reason: String,
108}
109
110#[derive(Debug, Clone, Default)]
124pub struct RestoreReport {
125 pub mtime_restored: usize,
127 pub mode_restored: usize,
129 pub unsupported: Vec<(UnsupportedKind, usize)>,
134 pub failures: Vec<RestoreFailure>,
136}
137
138impl RestoreReport {
139 #[must_use]
141 pub fn has_notes(&self) -> bool {
142 !self.unsupported.is_empty() || !self.failures.is_empty()
143 }
144
145 fn note_unsupported(&mut self, kind: UnsupportedKind) {
147 if let Some(slot) = self.unsupported.iter_mut().find(|(k, _)| *k == kind) {
148 slot.1 = slot.1.saturating_add(1);
149 } else {
150 self.unsupported.push((kind, 1));
151 }
152 }
153
154 pub fn note_unsupported_n(&mut self, kind: UnsupportedKind, n: usize) {
163 for _ in 0..n {
164 self.note_unsupported(kind);
165 }
166 }
167
168 pub fn push_failure(&mut self, path: String, item: &'static str, reason: String) {
173 self.failures.push(RestoreFailure { path, item, reason });
174 }
175}
176
177#[must_use]
198pub fn restore_metadata(root: &Path, idx: &ContainerIndex) -> RestoreReport {
199 let mut rep = RestoreReport::default();
200
201 for e in &idx.entries {
205 let p = root.join(e.path.join(std::path::MAIN_SEPARATOR_STR));
206 if !p.exists() {
207 continue;
209 }
210 apply(&p, &e.meta, &e.display_path(), &mut rep);
211 }
212
213 rep
214}
215
216fn apply(path: &Path, meta: &EntryMeta, display: &str, rep: &mut RestoreReport) {
218 if let Some(ns) = meta.mtime_ns {
219 match set_mtime(path, ns) {
220 Ok(()) => rep.mtime_restored = rep.mtime_restored.saturating_add(1),
221 Err(e) => rep.failures.push(RestoreFailure {
222 path: display.to_owned(),
223 item: "mtime",
224 reason: e.to_string(),
225 }),
226 }
227 }
228
229 if meta.btime_ns.is_some() {
230 rep.note_unsupported(UnsupportedKind::Btime);
236 }
237
238 if let Some(mode) = meta.mode {
239 match set_mode(path, mode) {
240 Ok(true) => rep.mode_restored = rep.mode_restored.saturating_add(1),
241 Ok(false) => rep.note_unsupported(UnsupportedKind::Mode),
242 Err(e) => rep.failures.push(RestoreFailure {
243 path: display.to_owned(),
244 item: "mode",
245 reason: e.to_string(),
246 }),
247 }
248 }
249
250 if meta.uid.is_some() || meta.gid.is_some() {
251 rep.note_unsupported(UnsupportedKind::Owner);
253 }
254
255 if !meta.xattrs.is_empty() {
256 rep.note_unsupported(UnsupportedKind::Xattr);
257 }
258}
259
260fn set_mtime(path: &Path, ns: i128) -> std::io::Result<()> {
265 use std::fs::FileTimes;
266
267 let t = ns_to_system_time(ns).ok_or_else(|| {
268 std::io::Error::new(
269 std::io::ErrorKind::InvalidInput,
270 format!("时间戳 {ns} 超出可表示范围"),
271 )
272 })?;
273
274 let f = open_for_times(path)?;
279 f.set_times(FileTimes::new().set_modified(t))
280}
281
282#[cfg(windows)]
286fn open_for_times(path: &Path) -> std::io::Result<std::fs::File> {
287 use std::os::windows::fs::OpenOptionsExt;
288 std::fs::OpenOptions::new()
293 .write(true)
294 .custom_flags(0x0200_0000)
295 .open(path)
296 .or_else(|_| {
297 std::fs::OpenOptions::new()
299 .read(true)
300 .custom_flags(0x0200_0000)
301 .open(path)
302 })
303}
304
305#[cfg(not(windows))]
307fn open_for_times(path: &Path) -> std::io::Result<std::fs::File> {
308 std::fs::OpenOptions::new()
309 .write(true)
310 .open(path)
311 .or_else(|_| std::fs::File::open(path))
312}
313
314#[cfg(unix)]
319fn set_mode(path: &Path, mode: u32) -> std::io::Result<bool> {
320 use std::os::unix::fs::PermissionsExt;
321 std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))?;
322 Ok(true)
323}
324
325#[cfg(not(unix))]
331fn set_mode(_path: &Path, _mode: u32) -> std::io::Result<bool> {
332 Ok(false)
333}
334
335fn ns_to_system_time(ns: i128) -> Option<std::time::SystemTime> {
340 use std::time::{Duration, UNIX_EPOCH};
341 let secs = ns.div_euclid(1_000_000_000);
342 let sub = ns.rem_euclid(1_000_000_000);
343 let sub_ns = u32::try_from(sub).ok()?;
344 if secs >= 0 {
345 let s = u64::try_from(secs).ok()?;
346 UNIX_EPOCH.checked_add(Duration::new(s, sub_ns))
347 } else {
348 let s = u64::try_from(secs.checked_neg()?).ok()?;
353 UNIX_EPOCH.checked_sub(Duration::new(s, 0))?.checked_add(
354 Duration::from_nanos(u64::from(sub_ns)),
355 )
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use crate::container::{ContainerBuilder, EntryMeta};
363
364 fn tmp(tag: &str) -> std::path::PathBuf {
365 let p = std::env::temp_dir().join(format!("omy-restore-{tag}"));
366 let _ = std::fs::remove_dir_all(&p);
367 std::fs::create_dir_all(&p).unwrap();
368 p
369 }
370
371 const T: i128 = 1_551_968_736_123_456_789;
373
374 #[test]
375 fn mtime_is_actually_written_to_disk() {
376 let root = tmp("mtime");
380 std::fs::write(root.join("a.txt"), b"x").unwrap();
381
382 let mut b = ContainerBuilder::new("r");
383 b.add_file(vec!["a.txt".into()], 1, None, EntryMeta {
384 mtime_ns: Some(T),
385 ..EntryMeta::default()
386 })
387 .unwrap();
388 let idx = b.finish().unwrap();
389
390 let rep = restore_metadata(&root, &idx);
391 assert_eq!(rep.mtime_restored, 1);
392
393 let got = std::fs::metadata(root.join("a.txt"))
394 .unwrap()
395 .modified()
396 .unwrap();
397 let want = ns_to_system_time(T).unwrap();
398 let d = got
399 .duration_since(want)
400 .unwrap_or_else(|e| e.duration())
401 .as_secs();
402 assert!(d < 2, "磁盘上的 mtime 应当接近 {want:?},实际 {got:?}");
403
404 let _ = std::fs::remove_dir_all(&root);
405 }
406
407 #[test]
408 fn dir_and_file_times_both_restored_in_real_extract_order() {
409 let root = tmp("dirorder");
412 std::fs::create_dir_all(root.join("sub")).unwrap();
413 std::fs::write(root.join("sub/c.txt"), b"c").unwrap();
414
415 let mut b = ContainerBuilder::new("r");
416 b.add_dir(vec!["sub".into()], EntryMeta {
417 mtime_ns: Some(T),
418 ..EntryMeta::default()
419 })
420 .unwrap();
421 b.add_file(vec!["sub".into(), "c.txt".into()], 1, None, EntryMeta {
422 mtime_ns: Some(T),
423 ..EntryMeta::default()
424 })
425 .unwrap();
426 let idx = b.finish().unwrap();
427
428 let rep = restore_metadata(&root, &idx);
429 assert_eq!(rep.mtime_restored, 2, "目录和文件都该设上");
430
431 let want = ns_to_system_time(T).unwrap();
432 for rel in ["sub", "sub/c.txt"] {
433 let p = root.join(rel.replace('/', std::path::MAIN_SEPARATOR_STR));
434 let got = std::fs::metadata(&p).unwrap().modified().unwrap();
435 let d = got
436 .duration_since(want)
437 .unwrap_or_else(|e| e.duration())
438 .as_secs();
439 assert!(d < 2, "{rel} 的 mtime 不对:{got:?}");
440 }
441
442 let _ = std::fs::remove_dir_all(&root);
443 }
444
445 #[test]
446 fn creating_entries_after_restore_clobbers_dir_mtime() {
447 let root = tmp("clobber");
455 std::fs::create_dir_all(root.join("sub")).unwrap();
456
457 let mut b = ContainerBuilder::new("r");
458 b.add_dir(vec!["sub".into()], EntryMeta {
459 mtime_ns: Some(T),
460 ..EntryMeta::default()
461 })
462 .unwrap();
463 let idx = b.finish().unwrap();
464
465 let _ = restore_metadata(&root, &idx);
466 let want = ns_to_system_time(T).unwrap();
467 let after_restore = std::fs::metadata(root.join("sub"))
468 .unwrap()
469 .modified()
470 .unwrap();
471 assert!(
472 after_restore
473 .duration_since(want)
474 .unwrap_or_else(|e| e.duration())
475 .as_secs()
476 < 2,
477 "还原后目录时间应当已设好"
478 );
479
480 std::fs::write(root.join("sub/late.txt"), b"x").unwrap();
482 let after_write = std::fs::metadata(root.join("sub"))
483 .unwrap()
484 .modified()
485 .unwrap();
486 assert_ne!(
487 after_restore, after_write,
488 "新建条目会冲掉目录 mtime——这正是必须最后还原的原因"
489 );
490
491 let before = after_write;
493 let mut b2 = ContainerBuilder::new("r");
494 b2.add_file(vec!["sub".into(), "late.txt".into()], 1, None, EntryMeta {
495 mtime_ns: Some(T),
496 ..EntryMeta::default()
497 })
498 .unwrap();
499 let _ = restore_metadata(&root, &b2.finish().unwrap());
500 let after_child_times = std::fs::metadata(root.join("sub"))
501 .unwrap()
502 .modified()
503 .unwrap();
504 assert_eq!(
505 before, after_child_times,
506 "设置子项时间不该改变父目录 mtime;若这条失败,按深度排序的\
507 复杂度就有必要重新加回来"
508 );
509
510 let _ = std::fs::remove_dir_all(&root);
511 }
512
513 #[test]
514 fn owner_is_reported_unsupported_not_silently_dropped() {
515 let root = tmp("owner");
518 std::fs::write(root.join("a.txt"), b"x").unwrap();
519
520 let mut b = ContainerBuilder::new("r");
521 b.add_file(vec!["a.txt".into()], 1, None, EntryMeta {
522 uid: Some(1000),
523 gid: Some(1000),
524 ..EntryMeta::default()
525 })
526 .unwrap();
527 let idx = b.finish().unwrap();
528
529 let rep = restore_metadata(&root, &idx);
530 assert!(rep.has_notes(), "不还原就必须报告");
531 assert_eq!(
532 rep.unsupported,
533 vec![(UnsupportedKind::Owner, 1)],
534 "应当只报 owner 一项"
535 );
536
537 let _ = std::fs::remove_dir_all(&root);
538 }
539
540 #[cfg(not(unix))]
541 #[test]
542 fn mode_reported_unsupported_on_windows_not_faked() {
543 let root = tmp("mode-win");
547 std::fs::write(root.join("a.txt"), b"x").unwrap();
548
549 let mut b = ContainerBuilder::new("r");
550 b.add_file(vec!["a.txt".into()], 1, None, EntryMeta {
551 mode: Some(0o644),
552 ..EntryMeta::default()
553 })
554 .unwrap();
555 let idx = b.finish().unwrap();
556
557 let rep = restore_metadata(&root, &idx);
558 assert_eq!(rep.mode_restored, 0, "不得谎报还原成功");
559 assert!(rep.failures.is_empty(), "平台不支持不是失败");
560 assert_eq!(rep.unsupported, vec![(UnsupportedKind::Mode, 1)]);
561
562 let _ = std::fs::remove_dir_all(&root);
563 }
564
565 #[cfg(unix)]
566 #[test]
567 fn mode_is_actually_applied_on_unix() {
568 use std::os::unix::fs::PermissionsExt;
569 let root = tmp("mode-unix");
570 std::fs::write(root.join("a.txt"), b"x").unwrap();
571
572 let mut b = ContainerBuilder::new("r");
573 b.add_file(vec!["a.txt".into()], 1, None, EntryMeta {
574 mode: Some(0o600),
575 ..EntryMeta::default()
576 })
577 .unwrap();
578 let idx = b.finish().unwrap();
579
580 let rep = restore_metadata(&root, &idx);
581 assert_eq!(rep.mode_restored, 1);
582 let m = std::fs::metadata(root.join("a.txt")).unwrap();
583 assert_eq!(m.permissions().mode() & 0o777, 0o600);
584
585 let _ = std::fs::remove_dir_all(&root);
586 }
587
588 #[test]
589 fn missing_entries_are_skipped_without_failure() {
590 let root = tmp("missing");
594
595 let mut b = ContainerBuilder::new("r");
596 b.add_file(vec!["ghost.txt".into()], 1, None, EntryMeta {
597 mtime_ns: Some(T),
598 ..EntryMeta::default()
599 })
600 .unwrap();
601 let idx = b.finish().unwrap();
602
603 let rep = restore_metadata(&root, &idx);
604 assert_eq!(rep.mtime_restored, 0);
605 assert!(rep.failures.is_empty(), "条目不存在不算还原失败");
606
607 let _ = std::fs::remove_dir_all(&root);
608 }
609
610 #[test]
611 fn empty_meta_produces_no_notes() {
612 let root = tmp("empty");
615 std::fs::write(root.join("a.txt"), b"x").unwrap();
616
617 let mut b = ContainerBuilder::new("r");
618 b.add_file(vec!["a.txt".into()], 1, None, EntryMeta::default())
619 .unwrap();
620 let idx = b.finish().unwrap();
621
622 let rep = restore_metadata(&root, &idx);
623 assert!(!rep.has_notes(), "无元数据时不该有任何提示");
624 assert_eq!(rep.mtime_restored, 0);
625
626 let _ = std::fs::remove_dir_all(&root);
627 }
628
629 #[test]
630 fn unsupported_counts_accumulate_across_entries() {
631 let root = tmp("accum");
634 for n in ["a", "b", "c"] {
635 std::fs::write(root.join(format!("{n}.txt")), b"x").unwrap();
636 }
637
638 let mut b = ContainerBuilder::new("r");
639 for n in ["a", "b", "c"] {
640 b.add_file(vec![format!("{n}.txt")], 1, None, EntryMeta {
641 uid: Some(1000),
642 ..EntryMeta::default()
643 })
644 .unwrap();
645 }
646 let idx = b.finish().unwrap();
647
648 let rep = restore_metadata(&root, &idx);
649 assert_eq!(rep.unsupported.len(), 1, "同一项只占一条");
650 assert_eq!(rep.unsupported[0].1, 3, "计数应累加到 3");
651
652 let _ = std::fs::remove_dir_all(&root);
653 }
654
655 #[test]
656 fn pre_epoch_timestamps_round_trip() {
657 let ns: i128 = -150_000_000_000_000_000; let t = ns_to_system_time(ns).expect("纪元前时间应可表示");
662 let back = std::time::UNIX_EPOCH
663 .duration_since(t)
664 .expect("应当早于纪元");
665 assert_eq!(
666 i128::try_from(back.as_nanos()).unwrap(),
667 -ns,
668 "纪元前时间戳往返后应当一致"
669 );
670 }
671
672 #[test]
673 fn sub_second_precision_is_not_dropped() {
674 let ns: i128 = 1_551_968_736_123_456_789;
685 let t = ns_to_system_time(ns).unwrap();
686 let d = t.duration_since(std::time::UNIX_EPOCH).unwrap();
687 let got = i64::from(d.subsec_nanos());
688 let diff = (got - 123_456_789_i64).abs();
689 assert!(
690 diff < 1_000,
691 "亚秒部分不能被丢弃:期望约 123456789ns,实际 {got}ns(差 {diff}ns)"
692 );
693 }
694
695 #[test]
696 fn unsupported_codes_are_stable() {
697 assert_eq!(UnsupportedKind::Mode.code(), "mode");
700 assert_eq!(UnsupportedKind::Btime.code(), "btime");
701 assert_eq!(UnsupportedKind::Owner.code(), "owner");
702 assert_eq!(UnsupportedKind::Xattr.code(), "xattr");
703 assert_eq!(UnsupportedKind::Symlink.code(), "symlink");
704 }
705}