Skip to main content

rok_utils/
path.rs

1//! Path helpers.
2
3use std::path::{Component, Path, PathBuf};
4
5/// Normalize `path` by resolving `.` and `..` components lexically (without
6/// touching the file system).  Absolute paths that walk above the root are
7/// clamped at the root.
8pub fn normalize<P: AsRef<Path>>(path: P) -> PathBuf {
9    let mut out = PathBuf::new();
10    for comp in path.as_ref().components() {
11        match comp {
12            Component::CurDir => {}
13            Component::ParentDir => {
14                // Only pop if there's something to pop (and it isn't a root/prefix).
15                match out.components().next_back() {
16                    Some(Component::RootDir | Component::Prefix(_)) | None => {}
17                    _ => {
18                        out.pop();
19                    }
20                }
21            }
22            other => out.push(other),
23        }
24    }
25    out
26}
27
28/// Return the file stem and extension as `(stem, ext)`.  Both are `""` when
29/// the component is absent.
30pub fn stem_ext<P: AsRef<Path>>(path: P) -> (String, String) {
31    let p = path.as_ref();
32    let stem = p
33        .file_stem()
34        .and_then(|s| s.to_str())
35        .unwrap_or("")
36        .to_string();
37    let ext = p
38        .extension()
39        .and_then(|s| s.to_str())
40        .unwrap_or("")
41        .to_string();
42    (stem, ext)
43}
44
45/// Replace the extension of `path` with `new_ext`.  If `new_ext` is empty the
46/// extension (and its leading `.`) is removed.
47pub fn with_extension<P: AsRef<Path>>(path: P, new_ext: &str) -> PathBuf {
48    path.as_ref().with_extension(new_ext)
49}
50
51// ── tests ────────────────────────────────────────────────────────────────────
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn normalize_removes_dots() {
59        assert_eq!(normalize("./foo/../bar/./baz"), PathBuf::from("bar/baz"));
60    }
61
62    #[test]
63    fn normalize_absolute() {
64        assert_eq!(normalize("/a/b/../c"), PathBuf::from("/a/c"));
65    }
66
67    #[test]
68    fn normalize_no_above_root() {
69        assert_eq!(normalize("/.."), PathBuf::from("/"));
70    }
71
72    #[test]
73    fn stem_ext_splits_correctly() {
74        assert_eq!(stem_ext("main.rs"), ("main".into(), "rs".into()));
75        assert_eq!(stem_ext("no_ext"), ("no_ext".into(), "".into()));
76    }
77}