1use std::path::{Component, Path, PathBuf};
7
8pub 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
24pub 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 assert!(joined.starts_with(base));
34 joined
35}
36
37pub 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
45pub 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
61pub fn has_symlinked_ancestor(root: &Path, path: &Path) -> bool {
69 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
89pub fn hex(bytes: &[u8]) -> String {
91 let mut s = String::with_capacity(bytes.len() * 2);
92 for byte in bytes {
93 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}