Skip to main content

agent_docs/
paths.rs

1use std::ffi::OsString;
2use std::path::{Component, Path, PathBuf};
3
4pub fn normalize_root_path(candidate: &Path, cwd: &Path) -> PathBuf {
5    if candidate.is_absolute() {
6        normalize_path(candidate)
7    } else {
8        normalize_path(&cwd.join(candidate))
9    }
10}
11
12pub fn normalize_path(path: &Path) -> PathBuf {
13    let mut prefix: Option<OsString> = None;
14    let mut has_root = false;
15    let mut segments: Vec<OsString> = Vec::new();
16
17    for component in path.components() {
18        match component {
19            Component::Prefix(value) => {
20                prefix = Some(value.as_os_str().to_os_string());
21            }
22            Component::RootDir => {
23                has_root = true;
24            }
25            Component::CurDir => {}
26            Component::ParentDir => {
27                if segments.pop().is_none() && !has_root {
28                    segments.push(OsString::from(".."));
29                }
30            }
31            Component::Normal(value) => {
32                segments.push(value.to_os_string());
33            }
34        }
35    }
36
37    let mut normalized = PathBuf::new();
38    if let Some(prefix) = prefix {
39        normalized.push(prefix);
40    }
41    if has_root {
42        normalized.push(Path::new(std::path::MAIN_SEPARATOR_STR));
43    }
44    for segment in segments {
45        normalized.push(segment);
46    }
47
48    if normalized.as_os_str().is_empty() {
49        PathBuf::from(".")
50    } else {
51        normalized
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::{normalize_path, normalize_root_path};
58    use std::path::Path;
59
60    #[test]
61    fn normalize_root_path_joins_relative_to_cwd() {
62        let normalized = normalize_root_path(Path::new("./docs/../policy"), Path::new("/tmp/repo"));
63        assert_eq!(normalized, Path::new("/tmp/repo/policy"));
64    }
65
66    #[test]
67    fn normalize_path_collapses_parent_segments_for_relative_paths() {
68        let normalized = normalize_path(Path::new("alpha/beta/../gamma/./delta"));
69        assert_eq!(normalized, Path::new("alpha/gamma/delta"));
70    }
71
72    #[test]
73    fn normalize_path_keeps_leading_parent_when_no_root() {
74        let normalized = normalize_path(Path::new("../alpha/../beta"));
75        assert_eq!(normalized, Path::new("../beta"));
76    }
77
78    #[test]
79    fn normalize_path_returns_dot_for_empty_input() {
80        let normalized = normalize_path(Path::new(""));
81        assert_eq!(normalized, Path::new("."));
82    }
83
84    #[cfg(windows)]
85    #[test]
86    fn normalize_path_collapses_segments_for_drive_absolute_paths() {
87        let normalized = normalize_path(Path::new(r"C:\repo\docs\..\policy\.\notes"));
88        assert_eq!(normalized, Path::new(r"C:\repo\policy\notes"));
89    }
90
91    #[cfg(windows)]
92    #[test]
93    fn normalize_path_keeps_drive_relative_prefix_without_root() {
94        let normalized = normalize_path(Path::new(r"C:repo\docs\..\policy"));
95        assert_eq!(normalized, Path::new(r"C:repo\policy"));
96    }
97
98    #[cfg(windows)]
99    #[test]
100    fn normalize_path_collapses_segments_for_unc_paths() {
101        let normalized = normalize_path(Path::new(r"\\server\share\alpha\..\beta\.\gamma"));
102        assert_eq!(normalized, Path::new(r"\\server\share\beta\gamma"));
103    }
104}