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}