Skip to main content

yui/
paths.rs

1//! Path utilities for backup-mirroring and timestamp suffixing.
2
3use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
4
5/// Mirror an absolute target path into a backup directory, dropping the drive
6/// colon on Windows so the path is filesystem-safe.
7///
8/// ```text
9///   C:\Users\u\foo.yml + .yui/backup → .yui/backup/C/Users/u/foo.yml
10///   /home/u/foo.yml    + .yui/backup → .yui/backup/home/u/foo.yml
11/// ```
12pub fn mirror_into_backup(backup_root: &Utf8Path, abs_target: &Utf8Path) -> Utf8PathBuf {
13    let mut out = backup_root.to_path_buf();
14    for component in abs_target.components() {
15        match component {
16            Utf8Component::Prefix(p) => {
17                let s = p.as_str().trim_end_matches(':');
18                if !s.is_empty() {
19                    out.push(s);
20                }
21            }
22            Utf8Component::RootDir | Utf8Component::CurDir => {}
23            Utf8Component::ParentDir => {}
24            Utf8Component::Normal(s) => {
25                out.push(s);
26            }
27        }
28    }
29    out
30}
31
32/// Append a timestamp before the extension.
33///
34/// ```text
35///   foo/bar.yml     + ts → foo/bar_<ts>.yml
36///   foo/bar         + ts → foo/bar_<ts>
37///   foo/.gitconfig  + ts → foo/.gitconfig_<ts>      (treat dotfiles as stem-only)
38/// ```
39pub fn append_timestamp(path: &Utf8Path, ts: &str) -> Utf8PathBuf {
40    let parent = path.parent().map(Utf8PathBuf::from).unwrap_or_default();
41    let file_name = path.file_name().unwrap_or("");
42
43    let (stem, ext) = match (path.file_stem(), path.extension()) {
44        (Some(stem), Some(ext)) if !file_name.starts_with('.') => (stem, Some(ext)),
45        _ => (file_name, None),
46    };
47
48    let new_name = match ext {
49        Some(ext) => format!("{stem}_{ts}.{ext}"),
50        None => format!("{stem}_{ts}"),
51    };
52    parent.join(new_name)
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn mirror_unix_absolute() {
61        let r = mirror_into_backup(
62            Utf8Path::new("/dotfiles/.yui/backup"),
63            Utf8Path::new("/home/u/.config/foo.toml"),
64        );
65        assert_eq!(
66            r,
67            Utf8PathBuf::from("/dotfiles/.yui/backup/home/u/.config/foo.toml")
68        );
69    }
70
71    #[test]
72    fn append_with_extension() {
73        let r = append_timestamp(Utf8Path::new("a/b.yml"), "20260429_143022123");
74        assert_eq!(r, Utf8PathBuf::from("a/b_20260429_143022123.yml"));
75    }
76
77    #[test]
78    fn append_no_extension() {
79        let r = append_timestamp(Utf8Path::new("a/b"), "20260429_143022123");
80        assert_eq!(r, Utf8PathBuf::from("a/b_20260429_143022123"));
81    }
82
83    #[test]
84    fn append_dotfile() {
85        let r = append_timestamp(Utf8Path::new(".gitconfig"), "20260429_143022123");
86        assert_eq!(r, Utf8PathBuf::from(".gitconfig_20260429_143022123"));
87    }
88}