Skip to main content

elfpak_core/
paths.rs

1//! Lexical path handling.
2//!
3//! Logical paths are interpreted relative to a source or output root, never
4//! passed directly to the host OS.
5
6use std::path::{Component, Path, PathBuf};
7
8/// Normalize a path into an absolute logical path.
9pub fn normalize_absolute(path: &Path) -> PathBuf {
10    let mut out = PathBuf::from("/");
11    for component in path.components() {
12        match component {
13            Component::RootDir | Component::Prefix(_) => {}
14            Component::CurDir => {}
15            Component::ParentDir => {
16                out.pop();
17            }
18            Component::Normal(part) => out.push(part),
19        }
20    }
21    out
22}
23
24/// Join a logical absolute path onto a real directory, refusing anything that
25/// would land outside of it.
26pub fn join_under(base: &Path, logical: &Path) -> PathBuf {
27    let normalized = normalize_absolute(logical);
28    let relative = normalized
29        .strip_prefix("/")
30        .expect("normalized logical paths are absolute");
31    let joined = base.join(relative);
32    // Containment is checked here, and again by the caller before it writes.
33    assert!(joined.starts_with(base));
34    joined
35}
36
37/// Absolute parent directory of a logical path (`/` for top-level entries).
38pub fn logical_parent(path: &Path) -> PathBuf {
39    assert!(path.is_absolute());
40    path.parent()
41        .map(Path::to_path_buf)
42        .unwrap_or_else(|| PathBuf::from("/"))
43}
44
45/// Every ancestor directory of a logical path, shallowest first, excluding `/`.
46pub fn ancestor_dirs(path: &Path) -> Vec<PathBuf> {
47    assert!(path.is_absolute());
48
49    let mut dirs = Vec::new();
50    let mut current = PathBuf::from("/");
51    let parent = logical_parent(path);
52    for component in parent.components() {
53        if let Component::Normal(part) = component {
54            current.push(part);
55            dirs.push(current.clone());
56        }
57    }
58    dirs
59}
60
61/// Whether any component between `root` and `path` is a symlink.
62///
63/// Checking only the immediate parent is not enough: `create_dir_all` and the
64/// verification walk both follow a symlink planted higher up. A walk that
65/// reaches the filesystem root without meeting `root` answers `true`, because a
66/// path that is not under the root it was supposed to be under is exactly the
67/// situation both callers exist to refuse.
68pub fn has_symlinked_ancestor(root: &Path, path: &Path) -> bool {
69    // `path` need not be under `root`: a manifest naming `/` produces a shorter
70    // path than the rootfs it is checked against. Every step drops one
71    // component, so the walk terminates on its own; the running of it off the
72    // top is the "not under the root" answer, not a broken invariant.
73    let mut current = path;
74    while current != root {
75        if std::fs::symlink_metadata(current).is_ok_and(|metadata| metadata.is_symlink()) {
76            return true;
77        }
78        let Some(parent) = current.parent() else {
79            return true;
80        };
81        if parent == current {
82            return true;
83        }
84        current = parent;
85    }
86    false
87}
88
89/// Render bytes as lowercase hex (used for digests).
90pub fn hex(bytes: &[u8]) -> String {
91    let mut s = String::with_capacity(bytes.len() * 2);
92    for byte in bytes {
93        // A nibble is always a valid hex digit, so neither `from_digit` can fail.
94        s.push(char::from_digit(u32::from(byte >> 4), 16).expect("a nibble is one hex digit"));
95        s.push(char::from_digit(u32::from(byte & 0xf), 16).expect("a nibble is one hex digit"));
96    }
97    s
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn normalizes_dot_and_parent_components() {
106        assert_eq!(
107            normalize_absolute(Path::new("/a/./b/../c")),
108            Path::new("/a/c")
109        );
110        assert_eq!(
111            normalize_absolute(Path::new("/../../etc")),
112            Path::new("/etc")
113        );
114        assert_eq!(normalize_absolute(Path::new("a/b")), Path::new("/a/b"));
115    }
116
117    #[test]
118    fn join_under_cannot_escape_the_base() {
119        let base = Path::new("/out");
120        assert_eq!(
121            join_under(base, Path::new("/etc/passwd")),
122            Path::new("/out/etc/passwd")
123        );
124        assert_eq!(
125            join_under(base, Path::new("/../../etc")),
126            Path::new("/out/etc")
127        );
128        assert_eq!(join_under(base, Path::new("/")), Path::new("/out"));
129    }
130
131    #[test]
132    fn ancestors_are_listed_shallowest_first() {
133        assert_eq!(
134            ancestor_dirs(Path::new("/usr/lib/x86_64-linux-gnu/libc.so.6")),
135            vec![
136                PathBuf::from("/usr"),
137                PathBuf::from("/usr/lib"),
138                PathBuf::from("/usr/lib/x86_64-linux-gnu"),
139            ]
140        );
141        assert!(ancestor_dirs(Path::new("/libc.so.6")).is_empty());
142    }
143
144    #[test]
145    fn hex_is_lowercase_and_padded() {
146        assert_eq!(hex(&[0x00, 0x0f, 0xff]), "000fff");
147    }
148}