use crate::container::{ContainerIndex, EntryMeta};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnsupportedKind {
Mode,
Btime,
Owner,
Xattr,
Symlink,
}
impl UnsupportedKind {
#[must_use]
pub const fn code(self) -> &'static str {
match self {
Self::Mode => "mode",
Self::Btime => "btime",
Self::Owner => "owner",
Self::Xattr => "xattr",
Self::Symlink => "symlink",
}
}
}
#[derive(Debug, Clone)]
pub struct RestoreFailure {
pub path: String,
pub item: &'static str,
pub reason: String,
}
#[derive(Debug, Clone, Default)]
pub struct RestoreReport {
pub mtime_restored: usize,
pub mode_restored: usize,
pub unsupported: Vec<(UnsupportedKind, usize)>,
pub failures: Vec<RestoreFailure>,
}
impl RestoreReport {
#[must_use]
pub fn has_notes(&self) -> bool {
!self.unsupported.is_empty() || !self.failures.is_empty()
}
fn note_unsupported(&mut self, kind: UnsupportedKind) {
if let Some(slot) = self.unsupported.iter_mut().find(|(k, _)| *k == kind) {
slot.1 = slot.1.saturating_add(1);
} else {
self.unsupported.push((kind, 1));
}
}
pub fn note_unsupported_n(&mut self, kind: UnsupportedKind, n: usize) {
for _ in 0..n {
self.note_unsupported(kind);
}
}
pub fn push_failure(&mut self, path: String, item: &'static str, reason: String) {
self.failures.push(RestoreFailure { path, item, reason });
}
}
#[must_use]
pub fn restore_metadata(root: &Path, idx: &ContainerIndex) -> RestoreReport {
let mut rep = RestoreReport::default();
for e in &idx.entries {
let p = root.join(e.path.join(std::path::MAIN_SEPARATOR_STR));
if !p.exists() {
continue;
}
apply(&p, &e.meta, &e.display_path(), &mut rep);
}
rep
}
fn apply(path: &Path, meta: &EntryMeta, display: &str, rep: &mut RestoreReport) {
if let Some(ns) = meta.mtime_ns {
match set_mtime(path, ns) {
Ok(()) => rep.mtime_restored = rep.mtime_restored.saturating_add(1),
Err(e) => rep.failures.push(RestoreFailure {
path: display.to_owned(),
item: "mtime",
reason: e.to_string(),
}),
}
}
if meta.btime_ns.is_some() {
rep.note_unsupported(UnsupportedKind::Btime);
}
if let Some(mode) = meta.mode {
match set_mode(path, mode) {
Ok(true) => rep.mode_restored = rep.mode_restored.saturating_add(1),
Ok(false) => rep.note_unsupported(UnsupportedKind::Mode),
Err(e) => rep.failures.push(RestoreFailure {
path: display.to_owned(),
item: "mode",
reason: e.to_string(),
}),
}
}
if meta.uid.is_some() || meta.gid.is_some() {
rep.note_unsupported(UnsupportedKind::Owner);
}
if !meta.xattrs.is_empty() {
rep.note_unsupported(UnsupportedKind::Xattr);
}
}
fn set_mtime(path: &Path, ns: i128) -> std::io::Result<()> {
use std::fs::FileTimes;
let t = ns_to_system_time(ns).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("时间戳 {ns} 超出可表示范围"),
)
})?;
let f = open_for_times(path)?;
f.set_times(FileTimes::new().set_modified(t))
}
#[cfg(windows)]
fn open_for_times(path: &Path) -> std::io::Result<std::fs::File> {
use std::os::windows::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.write(true)
.custom_flags(0x0200_0000)
.open(path)
.or_else(|_| {
std::fs::OpenOptions::new()
.read(true)
.custom_flags(0x0200_0000)
.open(path)
})
}
#[cfg(not(windows))]
fn open_for_times(path: &Path) -> std::io::Result<std::fs::File> {
std::fs::OpenOptions::new()
.write(true)
.open(path)
.or_else(|_| std::fs::File::open(path))
}
#[cfg(unix)]
fn set_mode(path: &Path, mode: u32) -> std::io::Result<bool> {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))?;
Ok(true)
}
#[cfg(not(unix))]
fn set_mode(_path: &Path, _mode: u32) -> std::io::Result<bool> {
Ok(false)
}
fn ns_to_system_time(ns: i128) -> Option<std::time::SystemTime> {
use std::time::{Duration, UNIX_EPOCH};
let secs = ns.div_euclid(1_000_000_000);
let sub = ns.rem_euclid(1_000_000_000);
let sub_ns = u32::try_from(sub).ok()?;
if secs >= 0 {
let s = u64::try_from(secs).ok()?;
UNIX_EPOCH.checked_add(Duration::new(s, sub_ns))
} else {
let s = u64::try_from(secs.checked_neg()?).ok()?;
UNIX_EPOCH.checked_sub(Duration::new(s, 0))?.checked_add(
Duration::from_nanos(u64::from(sub_ns)),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::container::{ContainerBuilder, EntryMeta};
fn tmp(tag: &str) -> std::path::PathBuf {
let p = std::env::temp_dir().join(format!("omy-restore-{tag}"));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
p
}
const T: i128 = 1_551_968_736_123_456_789;
#[test]
fn mtime_is_actually_written_to_disk() {
let root = tmp("mtime");
std::fs::write(root.join("a.txt"), b"x").unwrap();
let mut b = ContainerBuilder::new("r");
b.add_file(vec!["a.txt".into()], 1, None, EntryMeta {
mtime_ns: Some(T),
..EntryMeta::default()
})
.unwrap();
let idx = b.finish().unwrap();
let rep = restore_metadata(&root, &idx);
assert_eq!(rep.mtime_restored, 1);
let got = std::fs::metadata(root.join("a.txt"))
.unwrap()
.modified()
.unwrap();
let want = ns_to_system_time(T).unwrap();
let d = got
.duration_since(want)
.unwrap_or_else(|e| e.duration())
.as_secs();
assert!(d < 2, "磁盘上的 mtime 应当接近 {want:?},实际 {got:?}");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn dir_and_file_times_both_restored_in_real_extract_order() {
let root = tmp("dirorder");
std::fs::create_dir_all(root.join("sub")).unwrap();
std::fs::write(root.join("sub/c.txt"), b"c").unwrap();
let mut b = ContainerBuilder::new("r");
b.add_dir(vec!["sub".into()], EntryMeta {
mtime_ns: Some(T),
..EntryMeta::default()
})
.unwrap();
b.add_file(vec!["sub".into(), "c.txt".into()], 1, None, EntryMeta {
mtime_ns: Some(T),
..EntryMeta::default()
})
.unwrap();
let idx = b.finish().unwrap();
let rep = restore_metadata(&root, &idx);
assert_eq!(rep.mtime_restored, 2, "目录和文件都该设上");
let want = ns_to_system_time(T).unwrap();
for rel in ["sub", "sub/c.txt"] {
let p = root.join(rel.replace('/', std::path::MAIN_SEPARATOR_STR));
let got = std::fs::metadata(&p).unwrap().modified().unwrap();
let d = got
.duration_since(want)
.unwrap_or_else(|e| e.duration())
.as_secs();
assert!(d < 2, "{rel} 的 mtime 不对:{got:?}");
}
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn creating_entries_after_restore_clobbers_dir_mtime() {
let root = tmp("clobber");
std::fs::create_dir_all(root.join("sub")).unwrap();
let mut b = ContainerBuilder::new("r");
b.add_dir(vec!["sub".into()], EntryMeta {
mtime_ns: Some(T),
..EntryMeta::default()
})
.unwrap();
let idx = b.finish().unwrap();
let _ = restore_metadata(&root, &idx);
let want = ns_to_system_time(T).unwrap();
let after_restore = std::fs::metadata(root.join("sub"))
.unwrap()
.modified()
.unwrap();
assert!(
after_restore
.duration_since(want)
.unwrap_or_else(|e| e.duration())
.as_secs()
< 2,
"还原后目录时间应当已设好"
);
std::fs::write(root.join("sub/late.txt"), b"x").unwrap();
let after_write = std::fs::metadata(root.join("sub"))
.unwrap()
.modified()
.unwrap();
assert_ne!(
after_restore, after_write,
"新建条目会冲掉目录 mtime——这正是必须最后还原的原因"
);
let before = after_write;
let mut b2 = ContainerBuilder::new("r");
b2.add_file(vec!["sub".into(), "late.txt".into()], 1, None, EntryMeta {
mtime_ns: Some(T),
..EntryMeta::default()
})
.unwrap();
let _ = restore_metadata(&root, &b2.finish().unwrap());
let after_child_times = std::fs::metadata(root.join("sub"))
.unwrap()
.modified()
.unwrap();
assert_eq!(
before, after_child_times,
"设置子项时间不该改变父目录 mtime;若这条失败,按深度排序的\
复杂度就有必要重新加回来"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn owner_is_reported_unsupported_not_silently_dropped() {
let root = tmp("owner");
std::fs::write(root.join("a.txt"), b"x").unwrap();
let mut b = ContainerBuilder::new("r");
b.add_file(vec!["a.txt".into()], 1, None, EntryMeta {
uid: Some(1000),
gid: Some(1000),
..EntryMeta::default()
})
.unwrap();
let idx = b.finish().unwrap();
let rep = restore_metadata(&root, &idx);
assert!(rep.has_notes(), "不还原就必须报告");
assert_eq!(
rep.unsupported,
vec![(UnsupportedKind::Owner, 1)],
"应当只报 owner 一项"
);
let _ = std::fs::remove_dir_all(&root);
}
#[cfg(not(unix))]
#[test]
fn mode_reported_unsupported_on_windows_not_faked() {
let root = tmp("mode-win");
std::fs::write(root.join("a.txt"), b"x").unwrap();
let mut b = ContainerBuilder::new("r");
b.add_file(vec!["a.txt".into()], 1, None, EntryMeta {
mode: Some(0o644),
..EntryMeta::default()
})
.unwrap();
let idx = b.finish().unwrap();
let rep = restore_metadata(&root, &idx);
assert_eq!(rep.mode_restored, 0, "不得谎报还原成功");
assert!(rep.failures.is_empty(), "平台不支持不是失败");
assert_eq!(rep.unsupported, vec![(UnsupportedKind::Mode, 1)]);
let _ = std::fs::remove_dir_all(&root);
}
#[cfg(unix)]
#[test]
fn mode_is_actually_applied_on_unix() {
use std::os::unix::fs::PermissionsExt;
let root = tmp("mode-unix");
std::fs::write(root.join("a.txt"), b"x").unwrap();
let mut b = ContainerBuilder::new("r");
b.add_file(vec!["a.txt".into()], 1, None, EntryMeta {
mode: Some(0o600),
..EntryMeta::default()
})
.unwrap();
let idx = b.finish().unwrap();
let rep = restore_metadata(&root, &idx);
assert_eq!(rep.mode_restored, 1);
let m = std::fs::metadata(root.join("a.txt")).unwrap();
assert_eq!(m.permissions().mode() & 0o777, 0o600);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn missing_entries_are_skipped_without_failure() {
let root = tmp("missing");
let mut b = ContainerBuilder::new("r");
b.add_file(vec!["ghost.txt".into()], 1, None, EntryMeta {
mtime_ns: Some(T),
..EntryMeta::default()
})
.unwrap();
let idx = b.finish().unwrap();
let rep = restore_metadata(&root, &idx);
assert_eq!(rep.mtime_restored, 0);
assert!(rep.failures.is_empty(), "条目不存在不算还原失败");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn empty_meta_produces_no_notes() {
let root = tmp("empty");
std::fs::write(root.join("a.txt"), b"x").unwrap();
let mut b = ContainerBuilder::new("r");
b.add_file(vec!["a.txt".into()], 1, None, EntryMeta::default())
.unwrap();
let idx = b.finish().unwrap();
let rep = restore_metadata(&root, &idx);
assert!(!rep.has_notes(), "无元数据时不该有任何提示");
assert_eq!(rep.mtime_restored, 0);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn unsupported_counts_accumulate_across_entries() {
let root = tmp("accum");
for n in ["a", "b", "c"] {
std::fs::write(root.join(format!("{n}.txt")), b"x").unwrap();
}
let mut b = ContainerBuilder::new("r");
for n in ["a", "b", "c"] {
b.add_file(vec![format!("{n}.txt")], 1, None, EntryMeta {
uid: Some(1000),
..EntryMeta::default()
})
.unwrap();
}
let idx = b.finish().unwrap();
let rep = restore_metadata(&root, &idx);
assert_eq!(rep.unsupported.len(), 1, "同一项只占一条");
assert_eq!(rep.unsupported[0].1, 3, "计数应累加到 3");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn pre_epoch_timestamps_round_trip() {
let ns: i128 = -150_000_000_000_000_000; let t = ns_to_system_time(ns).expect("纪元前时间应可表示");
let back = std::time::UNIX_EPOCH
.duration_since(t)
.expect("应当早于纪元");
assert_eq!(
i128::try_from(back.as_nanos()).unwrap(),
-ns,
"纪元前时间戳往返后应当一致"
);
}
#[test]
fn sub_second_precision_is_not_dropped() {
let ns: i128 = 1_551_968_736_123_456_789;
let t = ns_to_system_time(ns).unwrap();
let d = t.duration_since(std::time::UNIX_EPOCH).unwrap();
let got = i64::from(d.subsec_nanos());
let diff = (got - 123_456_789_i64).abs();
assert!(
diff < 1_000,
"亚秒部分不能被丢弃:期望约 123456789ns,实际 {got}ns(差 {diff}ns)"
);
}
#[test]
fn unsupported_codes_are_stable() {
assert_eq!(UnsupportedKind::Mode.code(), "mode");
assert_eq!(UnsupportedKind::Btime.code(), "btime");
assert_eq!(UnsupportedKind::Owner.code(), "owner");
assert_eq!(UnsupportedKind::Xattr.code(), "xattr");
assert_eq!(UnsupportedKind::Symlink.code(), "symlink");
}
}