1#![forbid(unsafe_code)]
47#![warn(missing_docs)]
48
49use std::io;
50use std::path::{Path, PathBuf};
51
52#[cfg(target_os = "linux")]
53use std::path::Component;
54
55pub fn canonicalize(path: impl AsRef<Path>) -> io::Result<PathBuf> {
79 canonicalize_impl(path.as_ref())
80}
81
82#[cfg(target_os = "linux")]
83fn canonicalize_impl(path: &Path) -> io::Result<PathBuf> {
84 if let Some((namespace_prefix, remainder)) = find_namespace_boundary(path) {
86 if !namespace_prefix.exists() {
88 return Err(io::Error::new(
89 io::ErrorKind::NotFound,
90 format!("namespace path does not exist: {}", namespace_prefix.display()),
91 ));
92 }
93
94 if remainder.as_os_str().is_empty() {
95 Ok(namespace_prefix)
97 } else {
98 let full_path = namespace_prefix.join(&remainder);
101
102 let canonicalized = std::fs::canonicalize(&full_path)?;
105
106 Ok(namespace_prefix.join(canonicalized.strip_prefix("/").unwrap_or(&canonicalized)))
109 }
110 } else {
111 std::fs::canonicalize(path)
113 }
114}
115
116#[cfg(not(target_os = "linux"))]
117fn canonicalize_impl(path: &Path) -> io::Result<PathBuf> {
118 #[cfg(all(feature = "dunce", windows))]
120 {
121 dunce::canonicalize(path)
122 }
123 #[cfg(not(all(feature = "dunce", windows)))]
124 {
125 std::fs::canonicalize(path)
126 }
127}
128
129#[cfg(target_os = "linux")]
137fn find_namespace_boundary(path: &Path) -> Option<(PathBuf, PathBuf)> {
138 let mut components = path.components();
139
140 if components.next() != Some(Component::RootDir) {
142 return None;
143 }
144
145 match components.next() {
147 Some(Component::Normal(s)) if s == "proc" => {}
148 _ => return None,
149 }
150
151 let pid_component = match components.next() {
153 Some(Component::Normal(s)) => s,
154 _ => return None,
155 };
156
157 let pid_str = pid_component.to_string_lossy();
158 let is_valid_pid = pid_str == "self"
159 || pid_str == "thread-self"
160 || (!pid_str.is_empty() && pid_str.chars().all(|c| c.is_ascii_digit()));
161
162 if !is_valid_pid {
163 return None;
164 }
165
166 let ns_type = match components.next() {
168 Some(Component::Normal(s)) if s == "root" || s == "cwd" => s,
169 _ => return None,
170 };
171
172 let mut prefix = PathBuf::from("/proc");
174 prefix.push(pid_component);
175 prefix.push(ns_type);
176
177 let remainder: PathBuf = components.collect();
179
180 Some((prefix, remainder))
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[cfg(target_os = "linux")]
188 mod linux {
189 use super::*;
190
191 #[test]
192 fn test_find_namespace_boundary_proc_pid_root() {
193 let (prefix, remainder) =
194 find_namespace_boundary(Path::new("/proc/1234/root/etc/passwd")).unwrap();
195 assert_eq!(prefix, PathBuf::from("/proc/1234/root"));
196 assert_eq!(remainder, PathBuf::from("etc/passwd"));
197 }
198
199 #[test]
200 fn test_find_namespace_boundary_proc_pid_cwd() {
201 let (prefix, remainder) =
202 find_namespace_boundary(Path::new("/proc/5678/cwd/some/file.txt")).unwrap();
203 assert_eq!(prefix, PathBuf::from("/proc/5678/cwd"));
204 assert_eq!(remainder, PathBuf::from("some/file.txt"));
205 }
206
207 #[test]
208 fn test_find_namespace_boundary_proc_self_root() {
209 let (prefix, remainder) =
210 find_namespace_boundary(Path::new("/proc/self/root/etc/passwd")).unwrap();
211 assert_eq!(prefix, PathBuf::from("/proc/self/root"));
212 assert_eq!(remainder, PathBuf::from("etc/passwd"));
213 }
214
215 #[test]
216 fn test_find_namespace_boundary_proc_thread_self_root() {
217 let (prefix, remainder) =
218 find_namespace_boundary(Path::new("/proc/thread-self/root/app/config")).unwrap();
219 assert_eq!(prefix, PathBuf::from("/proc/thread-self/root"));
220 assert_eq!(remainder, PathBuf::from("app/config"));
221 }
222
223 #[test]
224 fn test_find_namespace_boundary_just_prefix() {
225 let (prefix, remainder) =
226 find_namespace_boundary(Path::new("/proc/1234/root")).unwrap();
227 assert_eq!(prefix, PathBuf::from("/proc/1234/root"));
228 assert_eq!(remainder, PathBuf::from(""));
229 }
230
231 #[test]
232 fn test_find_namespace_boundary_normal_path() {
233 assert!(find_namespace_boundary(Path::new("/home/user/file.txt")).is_none());
234 }
235
236 #[test]
237 fn test_find_namespace_boundary_proc_but_not_namespace() {
238 assert!(find_namespace_boundary(Path::new("/proc/1234/status")).is_none());
240 assert!(find_namespace_boundary(Path::new("/proc/1234/exe")).is_none());
241 assert!(find_namespace_boundary(Path::new("/proc/1234/fd/0")).is_none());
242 }
243
244 #[test]
245 fn test_find_namespace_boundary_relative_path() {
246 assert!(find_namespace_boundary(Path::new("proc/1234/root")).is_none());
247 }
248
249 #[test]
250 fn test_find_namespace_boundary_invalid_pid() {
251 assert!(find_namespace_boundary(Path::new("/proc/abc/root")).is_none());
252 assert!(find_namespace_boundary(Path::new("/proc/123abc/root")).is_none());
253 assert!(find_namespace_boundary(Path::new("/proc//root")).is_none());
254 }
255
256 #[test]
257 fn test_canonicalize_proc_self_root() {
258 let result = canonicalize("/proc/self/root").expect("should succeed");
260 assert_eq!(result, PathBuf::from("/proc/self/root"));
261
262 let std_result = std::fs::canonicalize("/proc/self/root").expect("should succeed");
264 assert_eq!(std_result, PathBuf::from("/"));
265
266 assert_ne!(result, std_result);
268 }
269
270 #[test]
271 fn test_canonicalize_proc_self_root_subpath() {
272 let result = canonicalize("/proc/self/root/etc").expect("should succeed");
274 assert!(
275 result.starts_with("/proc/self/root"),
276 "should preserve /proc/self/root prefix, got: {:?}",
277 result
278 );
279 }
280
281 #[test]
282 fn test_canonicalize_normal_path() {
283 let tmp = std::env::temp_dir();
285 let our_result = canonicalize(&tmp).expect("should succeed");
286 let std_result = std::fs::canonicalize(&tmp).expect("should succeed");
287 assert_eq!(our_result, std_result);
288 }
289
290 #[test]
291 fn test_canonicalize_proc_pid_root() {
292 use std::process;
293 let pid = process::id();
294 let proc_pid_root = format!("/proc/{}/root", pid);
295
296 let result = canonicalize(&proc_pid_root).expect("should succeed");
297 assert_eq!(result, PathBuf::from(&proc_pid_root));
298
299 let std_result = std::fs::canonicalize(&proc_pid_root).expect("should succeed");
301 assert_eq!(std_result, PathBuf::from("/"));
302 }
303 }
304
305 #[cfg(not(target_os = "linux"))]
306 mod non_linux {
307 use super::*;
308
309 #[test]
310 fn test_canonicalize_is_std_on_non_linux() {
311 let tmp = std::env::temp_dir();
313 let our_result = canonicalize(&tmp).expect("should succeed");
314 let std_result = std::fs::canonicalize(&tmp).expect("should succeed");
315 #[cfg(all(feature = "dunce", windows))]
317 {
318 let our_str = our_result.to_string_lossy();
319 let std_str = std_result.to_string_lossy();
320 assert!(!our_str.starts_with(r"\\?\"), "dunce should simplify path");
322 assert!(std_str.starts_with(r"\\?\"), "std returns UNC format");
323 assert_eq!(our_str.as_ref(), std_str.trim_start_matches(r"\\?\"));
325 }
326 #[cfg(not(all(feature = "dunce", windows)))]
328 {
329 assert_eq!(our_result, std_result);
330 }
331 }
332 }
333}