1use std::path::{Component, Path, PathBuf};
2
3#[derive(Debug, Clone)]
5pub struct NormalizeError {
6 pub message: &'static str,
7}
8
9impl std::fmt::Display for NormalizeError {
10 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11 write!(f, "Path normalization failed: {}", self.message)
12 }
13}
14
15impl std::error::Error for NormalizeError {}
16
17pub trait Normalize {
19 fn normalize(&self) -> Result<PathBuf, NormalizeError>;
25}
26
27impl Normalize for Path {
28 fn normalize(&self) -> Result<PathBuf, NormalizeError> {
29 let mut lexical = PathBuf::new();
30 let mut iter = self.components().peekable();
31
32 let root_len = match iter.peek() {
33 Some(Component::ParentDir) => {
34 return Err(NormalizeError {
35 message: "cannot start with ParentDir",
36 });
37 }
38 Some(Component::Prefix(prefix)) => {
39 lexical.push(prefix.as_os_str());
40 iter.next();
41 if let Some(Component::RootDir) = iter.peek() {
42 lexical.push(Component::RootDir);
43 iter.next();
44 }
45 lexical.as_os_str().len()
46 }
47 Some(Component::RootDir) | Some(Component::CurDir) => {
48 lexical.push(iter.next().unwrap());
49 lexical.as_os_str().len()
50 }
51 None => return Ok(PathBuf::new()),
52 Some(Component::Normal(_)) => 0,
53 };
54
55 for component in iter {
56 match component {
57 Component::RootDir => unreachable!(),
58 Component::Prefix(_) => {
59 return Err(NormalizeError {
60 message: "unexpected prefix component",
61 });
62 }
63 Component::CurDir => continue,
64 Component::ParentDir => {
65 if lexical.as_os_str().len() == root_len {
66 return Err(NormalizeError {
67 message: "attempted to traverse above root",
68 });
69 } else {
70 lexical.pop();
71 }
72 }
73 Component::Normal(path) => lexical.push(path),
74 }
75 }
76
77 Ok(lexical)
78 }
79}
80
81pub fn normalize(path: &Path) -> Result<PathBuf, NormalizeError> {
87 Normalize::normalize(path)
88}
89
90pub fn normalize_lossy(path: &Path) -> PathBuf {
92 path.normalize().unwrap_or_else(|_| path.to_path_buf())
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn test_normalize_basic() {
101 let path = Path::new("foo/./bar/../baz");
102 let normalized = path.normalize().unwrap();
103 assert_eq!(normalized, PathBuf::from("foo/baz"));
104 }
105
106 #[test]
107 fn test_normalize_rooted() {
108 let path = Path::new("/foo/../bar");
109 let normalized = path.normalize().unwrap();
110 assert_eq!(normalized, PathBuf::from("/bar"));
111 }
112
113 #[test]
114 fn test_normalize_error() {
115 let path = Path::new("../foo");
116 let err = path.normalize().unwrap_err();
117 assert_eq!(err.message, "cannot start with ParentDir");
118 }
119
120 #[test]
121 fn test_normalize_lossy() {
122 let path = Path::new("../foo");
123 let normalized = normalize_lossy(path);
124 assert_eq!(normalized, PathBuf::from("../foo"));
125 }
126}