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