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!(
91 "namespace path does not exist: {}",
92 namespace_prefix.display()
93 ),
94 ));
95 }
96
97 if remainder.as_os_str().is_empty() {
98 Ok(namespace_prefix)
100 } else {
101 let full_path = namespace_prefix.join(&remainder);
104
105 let canonicalized = std::fs::canonicalize(full_path)?;
108
109 Ok(namespace_prefix.join(canonicalized.strip_prefix("/").unwrap_or(&canonicalized)))
112 }
113 } else {
114 std::fs::canonicalize(path)
116 }
117}
118
119#[cfg(not(target_os = "linux"))]
120fn canonicalize_impl(path: &Path) -> io::Result<PathBuf> {
121 #[cfg(all(feature = "dunce", windows))]
123 {
124 dunce::canonicalize(path)
125 }
126 #[cfg(not(all(feature = "dunce", windows)))]
127 {
128 std::fs::canonicalize(path)
129 }
130}
131
132#[cfg(target_os = "linux")]
140fn find_namespace_boundary(path: &Path) -> Option<(PathBuf, PathBuf)> {
141 let mut components = path.components();
142
143 if components.next() != Some(Component::RootDir) {
145 return None;
146 }
147
148 match components.next() {
150 Some(Component::Normal(s)) if s == "proc" => {}
151 _ => return None,
152 }
153
154 let pid_component = match components.next() {
156 Some(Component::Normal(s)) => s,
157 _ => return None,
158 };
159
160 let pid_str = pid_component.to_string_lossy();
161 let is_valid_pid = pid_str == "self"
162 || pid_str == "thread-self"
163 || (!pid_str.is_empty() && pid_str.chars().all(|c| c.is_ascii_digit()));
164
165 if !is_valid_pid {
166 return None;
167 }
168
169 let ns_type = match components.next() {
171 Some(Component::Normal(s)) if s == "root" || s == "cwd" => s,
172 _ => return None,
173 };
174
175 let mut prefix = PathBuf::from("/proc");
177 prefix.push(pid_component);
178 prefix.push(ns_type);
179
180 let remainder: PathBuf = components.collect();
182
183 Some((prefix, remainder))
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[cfg(target_os = "linux")]
191 mod linux {
192 use super::*;
193
194 #[test]
195 fn test_find_namespace_boundary_proc_pid_root() {
196 let (prefix, remainder) =
197 find_namespace_boundary(Path::new("/proc/1234/root/etc/passwd")).unwrap();
198 assert_eq!(prefix, PathBuf::from("/proc/1234/root"));
199 assert_eq!(remainder, PathBuf::from("etc/passwd"));
200 }
201
202 #[test]
203 fn test_find_namespace_boundary_proc_pid_cwd() {
204 let (prefix, remainder) =
205 find_namespace_boundary(Path::new("/proc/5678/cwd/some/file.txt")).unwrap();
206 assert_eq!(prefix, PathBuf::from("/proc/5678/cwd"));
207 assert_eq!(remainder, PathBuf::from("some/file.txt"));
208 }
209
210 #[test]
211 fn test_find_namespace_boundary_proc_self_root() {
212 let (prefix, remainder) =
213 find_namespace_boundary(Path::new("/proc/self/root/etc/passwd")).unwrap();
214 assert_eq!(prefix, PathBuf::from("/proc/self/root"));
215 assert_eq!(remainder, PathBuf::from("etc/passwd"));
216 }
217
218 #[test]
219 fn test_find_namespace_boundary_proc_thread_self_root() {
220 let (prefix, remainder) =
221 find_namespace_boundary(Path::new("/proc/thread-self/root/app/config")).unwrap();
222 assert_eq!(prefix, PathBuf::from("/proc/thread-self/root"));
223 assert_eq!(remainder, PathBuf::from("app/config"));
224 }
225
226 #[test]
227 fn test_find_namespace_boundary_just_prefix() {
228 let (prefix, remainder) =
229 find_namespace_boundary(Path::new("/proc/1234/root")).unwrap();
230 assert_eq!(prefix, PathBuf::from("/proc/1234/root"));
231 assert_eq!(remainder, PathBuf::from(""));
232 }
233
234 #[test]
235 fn test_find_namespace_boundary_normal_path() {
236 assert!(find_namespace_boundary(Path::new("/home/user/file.txt")).is_none());
237 }
238
239 #[test]
240 fn test_find_namespace_boundary_proc_but_not_namespace() {
241 assert!(find_namespace_boundary(Path::new("/proc/1234/status")).is_none());
243 assert!(find_namespace_boundary(Path::new("/proc/1234/exe")).is_none());
244 assert!(find_namespace_boundary(Path::new("/proc/1234/fd/0")).is_none());
245 }
246
247 #[test]
248 fn test_find_namespace_boundary_relative_path() {
249 assert!(find_namespace_boundary(Path::new("proc/1234/root")).is_none());
250 }
251
252 #[test]
253 fn test_find_namespace_boundary_invalid_pid() {
254 assert!(find_namespace_boundary(Path::new("/proc/abc/root")).is_none());
255 assert!(find_namespace_boundary(Path::new("/proc/123abc/root")).is_none());
256 assert!(find_namespace_boundary(Path::new("/proc//root")).is_none());
257 }
258
259 #[test]
260 fn test_canonicalize_proc_self_root() {
261 let result = canonicalize("/proc/self/root").expect("should succeed");
263 assert_eq!(result, PathBuf::from("/proc/self/root"));
264
265 let std_result = std::fs::canonicalize("/proc/self/root").expect("should succeed");
267 assert_eq!(std_result, PathBuf::from("/"));
268
269 assert_ne!(result, std_result);
271 }
272
273 #[test]
274 fn test_canonicalize_proc_self_root_subpath() {
275 let result = canonicalize("/proc/self/root/etc").expect("should succeed");
277 assert!(
278 result.starts_with("/proc/self/root"),
279 "should preserve /proc/self/root prefix, got: {:?}",
280 result
281 );
282 }
283
284 #[test]
285 fn test_canonicalize_normal_path() {
286 let tmp = std::env::temp_dir();
288 let our_result = canonicalize(&tmp).expect("should succeed");
289 let std_result = std::fs::canonicalize(&tmp).expect("should succeed");
290 assert_eq!(our_result, std_result);
291 }
292
293 #[test]
294 fn test_canonicalize_proc_pid_root() {
295 use std::process;
296 let pid = process::id();
297 let proc_pid_root = format!("/proc/{}/root", pid);
298
299 let result = canonicalize(&proc_pid_root).expect("should succeed");
300 assert_eq!(result, PathBuf::from(&proc_pid_root));
301
302 let std_result = std::fs::canonicalize(&proc_pid_root).expect("should succeed");
304 assert_eq!(std_result, PathBuf::from("/"));
305 }
306
307 #[test]
308 fn test_canonicalize_proc_self_cwd() {
309 let result = canonicalize("/proc/self/cwd").expect("should succeed");
311 assert_eq!(result, PathBuf::from("/proc/self/cwd"));
312 }
313
314 #[test]
315 fn test_canonicalize_nonexistent_file_under_namespace() {
316 let result = canonicalize("/proc/self/root/this_file_definitely_does_not_exist_12345");
318 assert!(result.is_err());
319 let err = result.unwrap_err();
320 assert_eq!(err.kind(), io::ErrorKind::NotFound);
321 }
322
323 #[test]
324 fn test_canonicalize_nonexistent_pid() {
325 let result = canonicalize("/proc/4294967295/root");
327 assert!(result.is_err());
328 let err = result.unwrap_err();
329 assert_eq!(err.kind(), io::ErrorKind::NotFound);
330 }
331
332 #[test]
333 fn test_canonicalize_with_dotdot_normalization() {
334 let result = canonicalize("/proc/self/root/etc/../etc/passwd");
336 if let Ok(path) = result {
339 assert!(
340 path.starts_with("/proc/self/root"),
341 "should preserve namespace prefix, got: {:?}",
342 path
343 );
344 }
345 }
346
347 #[test]
348 fn test_canonicalize_with_dotdot_at_boundary() {
349 let result = canonicalize("/proc/self/root/tmp/../etc");
352 if let Ok(path) = result {
353 assert!(
354 path.starts_with("/proc/self/root"),
355 "should preserve namespace prefix even with .., got: {:?}",
356 path
357 );
358 }
359 }
360
361 #[test]
362 fn test_canonicalize_deep_nested_path() {
363 let result = canonicalize("/proc/self/root/usr/share/doc");
365 if let Ok(path) = result {
366 assert!(
367 path.starts_with("/proc/self/root"),
368 "should preserve namespace prefix for deep paths, got: {:?}",
369 path
370 );
371 }
372 }
373
374 #[test]
375 fn test_canonicalize_trailing_slash() {
376 let result = canonicalize("/proc/self/root/");
378 if let Ok(path) = result {
380 assert!(
381 path.starts_with("/proc/self/root"),
382 "should handle trailing slash, got: {:?}",
383 path
384 );
385 }
386 }
387
388 #[test]
389 fn test_canonicalize_thread_self() {
390 let result = canonicalize("/proc/thread-self/root");
392 if let Ok(path) = result {
393 assert_eq!(path, PathBuf::from("/proc/thread-self/root"));
394 }
395 }
397
398 #[test]
399 fn test_canonicalize_symlink_resolution_within_namespace() {
400 let result = canonicalize("/proc/self/root/etc/mtab");
403 if let Ok(path) = result {
404 assert!(
405 path.starts_with("/proc/self/root"),
406 "symlink resolution should preserve namespace, got: {:?}",
407 path
408 );
409 }
410 }
411
412 #[test]
413 fn test_find_namespace_boundary_with_trailing_slash() {
414 let result = find_namespace_boundary(Path::new("/proc/1234/root/"));
416 assert!(result.is_some());
417 let (prefix, _remainder) = result.unwrap();
418 assert_eq!(prefix, PathBuf::from("/proc/1234/root"));
419 }
421
422 #[test]
423 fn test_find_namespace_boundary_with_dots() {
424 let result = find_namespace_boundary(Path::new("/proc/1234/root/./etc/../etc"));
426 assert!(result.is_some());
427 let (prefix, _remainder) = result.unwrap();
428 assert_eq!(prefix, PathBuf::from("/proc/1234/root"));
429 }
431
432 #[test]
433 fn test_canonicalize_permission_denied() {
434 let result = canonicalize("/proc/1/root/etc/shadow");
437 if let Err(e) = result {
440 assert!(
441 e.kind() == io::ErrorKind::PermissionDenied
442 || e.kind() == io::ErrorKind::NotFound,
443 "expected PermissionDenied or NotFound, got: {:?}",
444 e.kind()
445 );
446 }
447 }
448
449 #[test]
450 fn test_canonicalize_pid_1_root() {
451 let result = canonicalize("/proc/1/root");
454
455 match result {
456 Ok(path) => {
457 assert_eq!(
459 path,
460 PathBuf::from("/proc/1/root"),
461 "must preserve /proc/1/root prefix"
462 );
463
464 let std_result =
466 std::fs::canonicalize("/proc/1/root").expect("std should also succeed");
467 assert_eq!(std_result, PathBuf::from("/"), "std resolves to /");
468 }
469 Err(e) => {
470 assert!(
472 e.kind() == io::ErrorKind::PermissionDenied
473 || e.kind() == io::ErrorKind::NotFound,
474 "expected PermissionDenied or NotFound, got: {:?}",
475 e.kind()
476 );
477 }
478 }
479 }
480
481 #[test]
482 fn test_canonicalize_pid_1_root_subpath() {
483 let result = canonicalize("/proc/1/root/etc/hostname");
485
486 match result {
487 Ok(path) => {
488 assert!(
490 path.starts_with("/proc/1/root"),
491 "must preserve /proc/1/root prefix, got: {:?}",
492 path
493 );
494 }
495 Err(e) => {
496 assert!(
498 e.kind() == io::ErrorKind::PermissionDenied
499 || e.kind() == io::ErrorKind::NotFound,
500 "expected PermissionDenied or NotFound, got: {:?}",
501 e.kind()
502 );
503 }
504 }
505 }
506
507 #[test]
508 fn test_canonicalize_pid_1_cwd() {
509 let result = canonicalize("/proc/1/cwd");
511
512 match result {
513 Ok(path) => {
514 assert_eq!(
515 path,
516 PathBuf::from("/proc/1/cwd"),
517 "must preserve /proc/1/cwd"
518 );
519 }
520 Err(e) => {
521 assert!(
522 e.kind() == io::ErrorKind::PermissionDenied
523 || e.kind() == io::ErrorKind::NotFound,
524 "expected PermissionDenied or NotFound, got: {:?}",
525 e.kind()
526 );
527 }
528 }
529 }
530
531 #[test]
532 fn test_self_vs_pid_equivalence() {
533 use std::process;
535 let pid = process::id();
536
537 let self_result = canonicalize("/proc/self/root").expect("self should work");
538 let pid_result = canonicalize(format!("/proc/{}/root", pid)).expect("pid should work");
539
540 assert_eq!(self_result, PathBuf::from("/proc/self/root"));
542 assert_eq!(pid_result, PathBuf::from(format!("/proc/{}/root", pid)));
543 }
544 }
545
546 #[cfg(not(target_os = "linux"))]
547 mod non_linux {
548 use super::*;
549
550 #[test]
551 fn test_canonicalize_is_std_on_non_linux() {
552 let tmp = std::env::temp_dir();
554 let our_result = canonicalize(&tmp).expect("should succeed");
555 let std_result = std::fs::canonicalize(&tmp).expect("should succeed");
556 #[cfg(all(feature = "dunce", windows))]
558 {
559 let our_str = our_result.to_string_lossy();
560 let std_str = std_result.to_string_lossy();
561 assert!(!our_str.starts_with(r"\\?\"), "dunce should simplify path");
563 assert!(std_str.starts_with(r"\\?\"), "std returns UNC format");
564 assert_eq!(our_str.as_ref(), std_str.trim_start_matches(r"\\?\"));
566 }
567 #[cfg(not(all(feature = "dunce", windows)))]
569 {
570 assert_eq!(our_result, std_result);
571 }
572 }
573 }
574}