#![forbid(unsafe_code)]
#![warn(missing_docs)]
use std::io;
use std::path::{Path, PathBuf};
#[cfg(target_os = "linux")]
use std::path::Component;
#[cfg(target_os = "linux")]
const MAX_SYMLINK_FOLLOWS: u32 = 40;
pub fn canonicalize(path: impl AsRef<Path>) -> io::Result<PathBuf> {
canonicalize_impl(path.as_ref())
}
#[cfg(target_os = "linux")]
fn canonicalize_impl(path: &Path) -> io::Result<PathBuf> {
if let Some((namespace_prefix, remainder)) = find_namespace_boundary(path) {
std::fs::metadata(&namespace_prefix)?;
if remainder.as_os_str().is_empty() {
Ok(namespace_prefix)
} else {
let resolved_prefix = std::fs::canonicalize(&namespace_prefix)?;
let full_path = namespace_prefix.join(&remainder);
let canonicalized = std::fs::canonicalize(full_path)?;
if let Ok(suffix) = canonicalized.strip_prefix(&resolved_prefix) {
Ok(namespace_prefix.join(suffix))
} else {
Ok(canonicalized)
}
}
} else {
if let Some(magic_path) = detect_indirect_proc_magic_link(path)? {
return canonicalize_impl(&magic_path);
}
std::fs::canonicalize(path)
}
}
#[cfg(not(target_os = "linux"))]
fn canonicalize_impl(path: &Path) -> io::Result<PathBuf> {
#[cfg(all(feature = "dunce", windows))]
{
dunce::canonicalize(path)
}
#[cfg(not(all(feature = "dunce", windows)))]
{
std::fs::canonicalize(path)
}
}
#[cfg(target_os = "linux")]
fn namespace_prefix_len(path: &Path) -> Option<usize> {
let mut components = path.components();
if components.next()? != Component::RootDir {
return None;
}
match components.next()? {
Component::Normal(s) if s == "proc" => {}
_ => return None,
}
let pid = match components.next()? {
Component::Normal(s) => s,
_ => return None,
};
if !is_valid_pid_segment(pid) {
return None;
}
let next = match components.next()? {
Component::Normal(s) => s,
_ => return None,
};
if next == "root" || next == "cwd" {
return Some(4);
}
if next != "task" {
return None;
}
let tid = match components.next()? {
Component::Normal(s) => s,
_ => return None,
};
if !is_numeric_segment(tid) {
return None;
}
match components.next()? {
Component::Normal(s) if s == "root" || s == "cwd" => Some(6),
_ => None,
}
}
#[cfg(target_os = "linux")]
fn is_valid_pid_segment(s: &std::ffi::OsStr) -> bool {
match s.to_str() {
Some("self") | Some("thread-self") => true,
Some(s) => is_nonempty_ascii_digits(s),
None => false,
}
}
#[cfg(target_os = "linux")]
fn is_numeric_segment(s: &std::ffi::OsStr) -> bool {
match s.to_str() {
Some(s) => is_nonempty_ascii_digits(s),
None => false,
}
}
#[cfg(target_os = "linux")]
fn is_nonempty_ascii_digits(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}
#[cfg(target_os = "linux")]
fn find_namespace_boundary(path: &Path) -> Option<(PathBuf, PathBuf)> {
let prefix_len = namespace_prefix_len(path)?;
let mut components = path.components();
let mut prefix = PathBuf::with_capacity(path.as_os_str().len());
for _ in 0..prefix_len {
prefix.push(components.next()?.as_os_str());
}
let remainder: PathBuf = components.collect();
Some((prefix, remainder))
}
#[cfg(target_os = "linux")]
fn is_proc_magic_path(path: &Path) -> bool {
namespace_prefix_len(path).is_some()
}
#[cfg(target_os = "linux")]
fn lexical_normalize_into(path: &Path, out: &mut PathBuf) {
out.clear();
for component in path.components() {
match component {
Component::RootDir => out.push(component.as_os_str()),
Component::Normal(name) => out.push(name),
Component::ParentDir => {
out.pop();
}
Component::CurDir => {}
Component::Prefix(_) => unreachable!("Linux paths don't have prefixes"),
}
}
}
#[cfg(target_os = "linux")]
fn detect_indirect_proc_magic_link(path: &Path) -> io::Result<Option<PathBuf>> {
let mut current_path = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()?.join(path)
};
let cap = current_path.as_os_str().len();
let mut accumulated = PathBuf::with_capacity(cap);
let mut normalized = PathBuf::with_capacity(cap);
let mut iterations = 0;
'scan: loop {
if iterations >= MAX_SYMLINK_FOLLOWS {
return Ok(None);
}
lexical_normalize_into(¤t_path, &mut normalized);
if is_proc_magic_path(&normalized) {
return Ok(Some(std::mem::take(&mut normalized)));
}
accumulated.clear();
let mut components = current_path.components().peekable();
if let Some(Component::RootDir) = components.peek() {
accumulated.push("/");
components.next();
}
while let Some(component) = components.next() {
match component {
Component::RootDir => {
accumulated.push("/");
}
Component::CurDir => {}
Component::ParentDir => {
accumulated.pop();
if is_proc_magic_path(&accumulated) {
accumulated.extend(components);
return Ok(Some(std::mem::take(&mut accumulated)));
}
}
Component::Normal(name) => {
accumulated.push(name);
let metadata = match std::fs::symlink_metadata(&accumulated) {
Ok(m) => m,
Err(_) => continue,
};
if metadata.is_symlink() {
iterations += 1;
let target = std::fs::read_link(&accumulated)?;
accumulated.pop(); accumulated.push(target);
accumulated.extend(components);
std::mem::swap(&mut current_path, &mut accumulated);
continue 'scan;
}
}
Component::Prefix(_) => unreachable!("Linux paths don't have prefixes"),
}
}
if is_proc_magic_path(&accumulated) {
return Ok(Some(std::mem::take(&mut accumulated)));
}
return Ok(None);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "linux")]
mod linux {
use super::*;
#[test]
fn test_find_namespace_boundary_proc_pid_root() {
let (prefix, remainder) =
find_namespace_boundary(Path::new("/proc/1234/root/etc/passwd")).unwrap();
assert_eq!(prefix, PathBuf::from("/proc/1234/root"));
assert_eq!(remainder, PathBuf::from("etc/passwd"));
}
#[test]
fn test_find_namespace_boundary_proc_pid_cwd() {
let (prefix, remainder) =
find_namespace_boundary(Path::new("/proc/5678/cwd/some/file.txt")).unwrap();
assert_eq!(prefix, PathBuf::from("/proc/5678/cwd"));
assert_eq!(remainder, PathBuf::from("some/file.txt"));
}
#[test]
fn test_find_namespace_boundary_proc_self_root() {
let (prefix, remainder) =
find_namespace_boundary(Path::new("/proc/self/root/etc/passwd")).unwrap();
assert_eq!(prefix, PathBuf::from("/proc/self/root"));
assert_eq!(remainder, PathBuf::from("etc/passwd"));
}
#[test]
fn test_find_namespace_boundary_proc_thread_self_root() {
let (prefix, remainder) =
find_namespace_boundary(Path::new("/proc/thread-self/root/app/config")).unwrap();
assert_eq!(prefix, PathBuf::from("/proc/thread-self/root"));
assert_eq!(remainder, PathBuf::from("app/config"));
}
#[test]
fn test_find_namespace_boundary_just_prefix_no_remainder() {
let (prefix, remainder) =
find_namespace_boundary(Path::new("/proc/1234/root")).unwrap();
assert_eq!(prefix, PathBuf::from("/proc/1234/root"));
assert_eq!(remainder, PathBuf::from(""));
}
#[test]
fn test_find_namespace_boundary_normal_path_returns_none() {
assert!(find_namespace_boundary(Path::new("/home/user/file.txt")).is_none());
}
#[test]
fn test_find_namespace_boundary_proc_other_files_not_namespace() {
assert!(find_namespace_boundary(Path::new("/proc/1234/status")).is_none());
assert!(find_namespace_boundary(Path::new("/proc/1234/exe")).is_none());
assert!(find_namespace_boundary(Path::new("/proc/1234/fd/0")).is_none());
}
#[test]
fn test_find_namespace_boundary_relative_path_rejected() {
assert!(find_namespace_boundary(Path::new("proc/1234/root")).is_none());
}
#[test]
fn test_find_namespace_boundary_invalid_pid_rejected() {
assert!(find_namespace_boundary(Path::new("/proc/abc/root")).is_none());
assert!(find_namespace_boundary(Path::new("/proc/123abc/root")).is_none());
assert!(find_namespace_boundary(Path::new("/proc//root")).is_none());
}
#[test]
fn boundary_detection_handles_trailing_slash() {
let (prefix, _remainder) =
find_namespace_boundary(Path::new("/proc/1234/root/")).unwrap();
assert_eq!(prefix, PathBuf::from("/proc/1234/root"));
}
#[test]
fn boundary_detection_handles_dot_components() {
let (prefix, _remainder) =
find_namespace_boundary(Path::new("/proc/1234/root/./etc/../etc")).unwrap();
assert_eq!(prefix, PathBuf::from("/proc/1234/root"));
}
#[test]
fn missing_pid_not_namespace() {
assert!(find_namespace_boundary(Path::new("/proc/root")).is_none());
}
#[test]
fn invalid_special_names_not_namespace() {
for name in &["parent", "init", "current", "me"] {
let path = format!("/proc/{name}/root");
assert!(find_namespace_boundary(Path::new(&path)).is_none());
}
}
#[test]
fn long_numeric_pid_accepted() {
let long_pid = "9".repeat(100);
let path = format!("/proc/{long_pid}/root");
assert!(find_namespace_boundary(Path::new(&path)).is_some());
}
#[test]
fn pid_zero_syntactically_valid_but_nonexistent() {
assert!(find_namespace_boundary(Path::new("/proc/0/root")).is_some());
assert!(canonicalize("/proc/0/root").is_err()); }
#[test]
fn negative_pid_not_valid() {
assert!(find_namespace_boundary(Path::new("/proc/-1/root")).is_none());
}
#[test]
fn leading_zeros_in_pid_accepted() {
assert!(find_namespace_boundary(Path::new("/proc/0001234/root")).is_some());
}
}
#[cfg(not(target_os = "linux"))]
mod non_linux {
use super::*;
#[test]
fn test_canonicalize_is_std_on_non_linux() {
let tmp = std::env::temp_dir();
let our_result = canonicalize(&tmp).expect("should succeed");
let std_result = std::fs::canonicalize(&tmp).expect("should succeed");
#[cfg(all(feature = "dunce", windows))]
{
let our_str = our_result.to_string_lossy();
let std_str = std_result.to_string_lossy();
assert!(!our_str.starts_with(r"\\?\"), "dunce should simplify path");
assert!(std_str.starts_with(r"\\?\"), "std returns UNC format");
assert_eq!(our_str.as_ref(), std_str.trim_start_matches(r"\\?\"));
}
#[cfg(not(all(feature = "dunce", windows)))]
{
assert_eq!(our_result, std_result);
}
}
}
}