1#[derive(Debug, Clone, PartialEq, Eq)]
2enum Component {
3 UriScheme(String),
4 DrivePrefix(String),
5 Root,
6 CurrentDir,
7 ParentDir,
8 Normal(String),
9}
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Path {
13 separator: char,
14 components: Vec<Component>,
15}
16
17impl Path {
18 pub fn new(path: impl Into<String>) -> Path {
19 let path: String = path.into();
20 let mut path = path.as_str();
21 let mut components = Vec::with_capacity(4);
22 let separator = match drive_prefix(path) {
23 Some(prefix) => {
24 components.push(Component::DrivePrefix(prefix));
25 path = &path[2..];
26 '\\'
27 }
28 _ => {
29 if path.contains('\\') {
30 '\\'
31 } else if path.contains('/') {
32 '/'
33 } else {
34 std::path::MAIN_SEPARATOR
35 }
36 }
37 };
38 if path.starts_with(separator) {
39 components.push(Component::Root);
40 path = &path[1..];
41 }
42 path.split(separator).for_each(|s| match s {
43 "" => {}
44 "." => components.push(Component::CurrentDir),
45 ".." => components.push(Component::ParentDir),
46 "https:" => components.push(Component::UriScheme("https".to_string())),
47 "http:" => components.push(Component::UriScheme("http".to_string())),
48 _ => components.push(Component::Normal(s.to_string())),
49 });
50 Path { separator, components }
51 }
52
53 pub fn push(&mut self, other: impl Into<Path>) {
54 let other: Path = other.into();
55 if other.is_absolute() {
56 self.components.clear();
57 }
58 self.components.extend(other.components);
59 }
60
61 pub fn is_absolute(&self) -> bool {
62 if self.is_uri() && matches!(self.components.first(), Some(Component::UriScheme(_))) {
63 return true;
64 }
65 self.components.first() == Some(&Component::Root)
66 || (matches!(self.components.first(), Some(Component::DrivePrefix(_)))
67 && matches!(self.components.get(1), Some(Component::Root)))
68 }
69
70 pub fn is_relative(&self) -> bool {
71 !self.is_absolute()
72 }
73
74 pub fn pop(&mut self) -> bool {
75 if self.components.len() > 1 {
76 self.components.pop();
77 true
78 } else {
79 false
80 }
81 }
82
83 pub fn join(&self, other: impl Into<Path>) -> Path {
84 let mut joined = self.clone();
85 joined.push(other);
86 joined
87 }
88
89 pub fn file_name(&self) -> &str {
90 self._file_name(self.components.len() - 1)
91 }
92
93 fn _file_name(&self, idx: usize) -> &str {
94 match self.components.get(idx) {
95 Some(Component::Normal(s)) => s,
96 Some(Component::CurrentDir) => self._file_name(idx - 1),
97 _ => "",
98 }
99 }
100
101 pub fn file_stem(&self) -> &str {
102 let file_name = self.file_name();
103 file_name
104 .rsplit_once('.')
105 .map(|(before, _)| before)
106 .unwrap_or(file_name)
107 }
108
109 pub fn extension(&self) -> &str {
110 let filename = self.file_name();
111 if let Some(idx) = filename.rfind('.') {
112 &filename[idx..]
113 } else {
114 ""
115 }
116 }
117
118 pub fn dirname(&self) -> String {
119 if self.components.len() == 1 && self.components[0] == Component::Root {
120 return self.to_string();
121 }
122 if self.components.len() == 2
123 && matches!(self.components[0], Component::DrivePrefix(_))
124 && self.components[1] == Component::Root
125 {
126 return self.to_string();
127 }
128 let mut path = String::with_capacity(32);
129 for (i, component) in self.components.iter().enumerate() {
130 if i == self.components.len() - 1 {
131 break;
132 }
133 match component {
134 Component::UriScheme(s) => {
135 path.push_str(s);
136 path.push(':');
137 }
138 Component::DrivePrefix(s) => path.push_str(s),
139 Component::Root => path.push(self.separator),
140 Component::CurrentDir => path.push('.'),
141 Component::ParentDir => path.push_str(".."),
142 Component::Normal(s) => path.push_str(s),
143 }
144 if i < self.components.len() - 2
145 && component != &Component::Root
146 && !matches!(component, Component::DrivePrefix(_))
147 {
148 path.push(self.separator);
149 }
150 }
151 path
152 }
153
154 pub fn is_uri(&self) -> bool {
155 matches!(self.components.first(), Some(Component::UriScheme(_)))
156 }
157}
158
159impl From<std::path::PathBuf> for Path {
160 fn from(path: std::path::PathBuf) -> Self {
161 Path::new(path.to_string_lossy())
162 }
163}
164
165impl From<&str> for Path {
166 fn from(path: &str) -> Self {
167 Path::new(path)
168 }
169}
170
171impl std::fmt::Display for Path {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 let mut path = String::with_capacity(32);
174 for (i, component) in self.components.iter().enumerate() {
175 match component {
176 Component::UriScheme(s) => {
177 path.push_str(s);
178 path.push_str(":/");
179 }
180 Component::DrivePrefix(s) => path.push_str(s),
181 Component::Root => path.push(self.separator),
182 Component::CurrentDir => path.push('.'),
183 Component::ParentDir => path.push_str(".."),
184 Component::Normal(s) => path.push_str(s),
185 }
186 if i < self.components.len() - 1
187 && component != &Component::Root
188 && !matches!(component, Component::DrivePrefix(_))
189 {
190 path.push(self.separator);
191 }
192 }
193 write!(f, "{}", path)
194 }
195}
196
197fn drive_prefix(path: &str) -> Option<String> {
198 if path.len() < 2 {
199 return None;
200 }
201 let bytes = path.as_bytes();
202 if bytes[1] == b':' && bytes[0].is_ascii_alphabetic() {
203 let prefix = path[..2].to_string();
204 Some(prefix)
205 } else {
206 None
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 fn path(s: &str) -> Path {
215 Path::new(s)
216 }
217
218 #[test]
219 fn path_new() {
220 let path = Path::new("/usr/local");
221 assert_eq!(path.separator, '/');
222 assert_eq!(path.components.len(), 3);
223 assert_eq!(path.components[0], Component::Root);
224 assert_eq!(path.components[1], Component::Normal("usr".to_string()));
225 assert_eq!(path.components[2], Component::Normal("local".to_string()));
226 assert_eq!(path.to_string(), "/usr/local");
227 let path = Path::new("/usr/local/");
228 assert_eq!(path.to_string(), "/usr/local");
229 }
230
231 #[test]
232 fn path_new_windows() {
233 let mut path = Path::new(r#"c:\windows\foo.dll"#);
234 assert_eq!(path.separator, '\\');
235 assert_eq!(path.components.len(), 4);
236 assert_eq!(path.components[0], Component::DrivePrefix("c:".to_string()));
237 assert_eq!(path.components[1], Component::Root);
238 assert_eq!(path.components[2], Component::Normal("windows".to_string()));
239 assert_eq!(path.components[3], Component::Normal("foo.dll".to_string()));
240 assert_eq!(path.to_string(), r#"c:\windows\foo.dll"#);
241 path.pop();
242 path.push("baz");
243 path.push("qux.dll");
244 assert_eq!(path.to_string(), r#"c:\windows\baz\qux.dll"#);
245 assert!(!path.is_uri());
246 }
247
248 #[test]
249 fn path_new_uri() {
250 let path = Path::new(r#"https://example.com/path"#);
251 assert_eq!(path.separator, '/');
252 assert_eq!(path.components.len(), 3);
253 assert_eq!(
254 path.components[0],
255 Component::UriScheme("https".to_string())
256 );
257 assert_eq!(
258 path.components[1],
259 Component::Normal("example.com".to_string())
260 );
261 assert_eq!(path.components[2], Component::Normal("path".to_string()));
262 assert_eq!(path.to_string(), "https://example.com/path");
263 assert!(path.is_uri());
264 }
265
266 #[test]
267 fn path_is_absolute() {
268 assert!(path("/usr/local").is_absolute());
269 assert!(!path("usr/local").is_absolute());
270 assert!(path(r#"c:\foo"#).is_absolute());
271 assert!(path(r#"\foo"#).is_absolute());
272 assert!(path(r#"http://foo.com"#).is_absolute());
273 assert!(!path(r#"c:foo"#).is_absolute());
274 }
275
276 #[test]
277 fn path_push_pop() {
278 let mut path = Path::new("/usr/local");
279 path.push("bin");
280 assert_eq!(path.components.len(), 4);
281 assert_eq!(path.components[3], Component::Normal("bin".to_string()));
282 assert_eq!(path.to_string(), "/usr/local/bin");
283 assert!(path.pop());
284 assert_eq!(path.to_string(), "/usr/local");
285 assert!(path.pop());
286 assert_eq!(path.to_string(), "/usr");
287 assert!(path.pop());
288 assert_eq!(path.to_string(), "/");
289 assert!(!path.pop());
290 assert_eq!(path.to_string(), "/");
291 let mut path = Path::new("/usr/local");
293 path.push("/bin");
294 assert_eq!(path.to_string(), "/bin");
295 }
296
297 #[test]
298 fn path_join() {
299 let path = Path::new("/etc");
300 assert_eq!(path.to_string(), "/etc");
301 let joined = path.join("passwd");
302 assert_eq!(path.to_string(), "/etc");
303 assert_eq!(joined.to_string(), "/etc/passwd");
304 }
305
306 #[test]
307 fn path_dirname() {
308 assert_eq!("", &path("foo.txt").dirname());
309 assert_eq!("bar", &path("bar/foo.txt").dirname());
310 assert_eq!("/", &path("/foo.txt").dirname());
311 assert_eq!("/", &path("/").dirname());
312 assert_eq!("c:\\foo", path("c:\\foo\\baz.adoc").dirname());
313 assert_eq!("c:\\", path("c:\\").dirname());
314 }
315
316 #[test]
317 fn path_extension() {
318 assert_eq!(".txt", path("foo/bar/baz.txt").extension());
319 assert_eq!(".asciidoc", path("foo/bar/baz.asciidoc").extension());
320 assert_eq!(".txt", path("baz.txt").extension());
321 assert_eq!("", path("foo/bar/baz").extension());
322 assert_eq!("", path("foo").extension());
323 assert_eq!("", path("foo/b.ar/baz").extension());
324 }
325
326 #[test]
327 fn path_file_name() {
328 assert_eq!("bin", path("bin").file_name());
329 assert_eq!("bin", path("bin/").file_name());
330 assert_eq!("foo.txt", path("tmp/foo.txt").file_name());
331 assert_eq!("foo.txt", path("foo.txt/.").file_name());
332 assert_eq!("foo.txt", path("foo.txt/./././././.").file_name());
333 assert_eq!("foo.txt", path("foo.txt/.//").file_name());
334 assert_eq!("", path("foo.txt/..").file_name());
335 assert_eq!("", path("/").file_name());
336 assert_eq!("", path("c:\\").file_name());
337 assert_eq!("foo", path("c:\\foo").file_name());
338 assert_eq!("foo", path("\\foo").file_name());
339 }
340
341 #[test]
342 fn path_file_stem() {
343 assert_eq!("bin", path("bin").file_stem());
344 assert_eq!("bin", path("bin/").file_stem());
345 assert_eq!("foo", path("foo.rs").file_stem());
346 assert_eq!("foo", path("/weird.txt/foo.bar/foo.rs").file_stem());
347 assert_eq!("foo.tar", path("foo.tar.gz").file_stem());
348 }
349
350 #[test]
351 fn join_uri_relative() {
352 let src = Path::new("https://example.com/foo/bar");
353 let dir = Path::new(src.dirname());
354 assert_eq!(dir.to_string(), "https://example.com/foo");
355 let rel = Path::new("baz");
356 let abs = dir.join(rel);
357 assert_eq!(abs.to_string(), "https://example.com/foo/baz");
358 assert!(abs.is_uri());
359 }
360}