use std::path::{Path, PathBuf};
use std::{fs, io};
use crate::error::error_with_path;
use crate::normalize::simple_normalize_path;
#[doc(alias = "ELOOP")]
#[doc(alias = "symlink_limit")]
pub const MAX_SYMLINK_DEPTH: usize = if cfg!(target_os = "windows") { 63 } else { 40 };
#[cfg(feature = "anchored")]
pub(crate) fn strip_root_prefix(path: &Path) -> PathBuf {
#[cfg(unix)]
{
path.strip_prefix("/").unwrap_or(path).to_path_buf()
}
#[cfg(windows)]
{
use std::path::Component;
let mut result = PathBuf::new();
for comp in path.components() {
match comp {
Component::Prefix(_) | Component::RootDir => continue,
_ => result.push(comp),
}
}
if result.as_os_str().is_empty() {
PathBuf::from(".")
} else {
result
}
}
}
#[cfg(feature = "anchored")]
#[inline]
pub(crate) fn component_eq(a: &std::path::Component, b: &std::path::Component) -> bool {
#[cfg(windows)]
{
use std::path::{Component, Prefix};
match (a, b) {
(Component::Prefix(ap), Component::Prefix(bp)) => match (ap.kind(), bp.kind()) {
(Prefix::VerbatimDisk(ad), Prefix::Disk(bd))
| (Prefix::Disk(ad), Prefix::VerbatimDisk(bd))
| (Prefix::VerbatimDisk(ad), Prefix::VerbatimDisk(bd))
| (Prefix::Disk(ad), Prefix::Disk(bd)) => ad.eq_ignore_ascii_case(&bd),
(Prefix::VerbatimUNC(as1, as2), Prefix::UNC(bs1, bs2))
| (Prefix::UNC(as1, as2), Prefix::VerbatimUNC(bs1, bs2))
| (Prefix::VerbatimUNC(as1, as2), Prefix::VerbatimUNC(bs1, bs2))
| (Prefix::UNC(as1, as2), Prefix::UNC(bs1, bs2)) => {
as1.eq_ignore_ascii_case(bs1) && as2.eq_ignore_ascii_case(bs2)
}
_ => ap == bp,
},
_ => a == b,
}
}
#[cfg(not(windows))]
{
a == b
}
}
#[cfg(feature = "anchored")]
pub(crate) fn normalize_and_clamp_to_anchor(current: &Path, anchor: &Path) -> (PathBuf, bool) {
let normalized = simple_normalize_path(current);
let anchor_comps: Vec<_> = anchor.components().collect();
let current_comps: Vec<_> = normalized.components().collect();
let is_within_anchor = current_comps.len() >= anchor_comps.len()
&& current_comps
.iter()
.zip(anchor_comps.iter())
.all(|(c, a)| component_eq(c, a));
if is_within_anchor {
(normalized, false)
} else {
let mut common_depth = 0;
for (a, c) in anchor_comps.iter().zip(current_comps.iter()) {
if component_eq(a, c) {
common_depth += 1;
} else {
break;
}
}
let mut clamped = anchor.to_path_buf();
for comp in current_comps.iter().skip(common_depth) {
clamped.push(comp);
}
(clamped, true)
}
}
#[cfg(feature = "anchored")]
pub(crate) fn resolve_anchored_symlink_chain(
symlink_path: &Path,
anchor: &Path,
) -> io::Result<PathBuf> {
let mut current = symlink_path.to_path_buf();
let mut depth = 0usize;
let mut visited: Vec<std::ffi::OsString> = Vec::with_capacity(8);
let effective_max_depth = if is_likely_system_symlink(¤t) {
5
} else {
MAX_SYMLINK_DEPTH
};
loop {
if visited.iter().any(|s| s == current.as_os_str()) {
return Err(error_with_path(
io::ErrorKind::InvalidInput,
symlink_path,
"Too many levels of symbolic links",
));
}
visited.push(current.as_os_str().to_os_string());
match fs::read_link(¤t) {
Ok(target) => {
depth += 1;
if depth > effective_max_depth {
return Err(error_with_path(
io::ErrorKind::InvalidInput,
symlink_path,
"Too many levels of symbolic links",
));
}
if target.is_absolute() {
let target = target.clone();
#[cfg(windows)]
let rel_path = {
let target_for_comparison =
if let Ok(canonical_target) = std::fs::canonicalize(&target) {
canonical_target
} else {
let mut check = target.clone();
let mut suffix_parts = Vec::new();
while !check.as_os_str().is_empty() {
if check.exists() {
break;
}
if let Some(file_name) = check.file_name() {
suffix_parts.push(file_name.to_os_string());
}
if !check.pop() {
break;
}
}
if let Ok(canonical_prefix) = std::fs::canonicalize(&check) {
let mut result = canonical_prefix;
for part in suffix_parts.into_iter().rev() {
result.push(part);
}
result
} else {
target.clone()
}
};
let anchor_comps: Vec<_> = anchor.components().collect();
let target_comps: Vec<_> = target_for_comparison.components().collect();
let is_within_anchor = target_comps.len() >= anchor_comps.len()
&& target_comps
.iter()
.zip(anchor_comps.iter())
.all(|(t, a)| component_eq(t, a));
if is_within_anchor {
let mut rel = std::path::PathBuf::new();
for comp in target_comps.iter().skip(anchor_comps.len()) {
rel.push(comp);
}
Some(rel)
} else {
None
}
};
#[cfg(not(windows))]
let rel_path = target.strip_prefix(anchor).ok().map(|p| p.to_path_buf());
let tentative = if let Some(rel) = rel_path {
anchor.join(rel)
} else {
let stripped = strip_root_prefix(&target);
anchor.join(stripped)
};
let (clamped, _was_clamped) = normalize_and_clamp_to_anchor(&tentative, anchor);
current = clamped;
} else {
let parent = current.parent();
if let Some(p) = parent {
let joined = p.join(target);
let (clamped, _was_clamped) =
normalize_and_clamp_to_anchor(&joined, anchor);
current = clamped;
#[cfg(windows)]
if !_was_clamped && current.exists() {
use std::path::Prefix;
let anchor_has_prefix = anchor.components().next().is_some_and(|c| {
matches!(c, std::path::Component::Prefix(p) if matches!(
p.kind(),
Prefix::VerbatimDisk(_) | Prefix::VerbatimUNC(_, _) | Prefix::Verbatim(_)
))
});
let current_has_prefix = current.components().next().is_some_and(|c| {
matches!(c, std::path::Component::Prefix(p) if matches!(
p.kind(),
Prefix::VerbatimDisk(_) | Prefix::VerbatimUNC(_, _) | Prefix::Verbatim(_)
))
});
if anchor_has_prefix && !current_has_prefix {
if let Ok(canonicalized) = std::fs::canonicalize(¤t) {
current = canonicalized;
}
}
}
} else {
current = target.clone();
}
}
}
Err(_e) => {
break; }
}
}
Ok(current)
}
#[inline]
pub(crate) fn resolve_simple_symlink_chain(symlink_path: &Path) -> io::Result<PathBuf> {
let mut current = symlink_path.to_path_buf();
let mut depth = 0usize;
let mut visited: Vec<std::ffi::OsString> = Vec::with_capacity(8);
let effective_max_depth = if is_likely_system_symlink(¤t) {
5
} else {
MAX_SYMLINK_DEPTH
};
loop {
if visited.iter().any(|s| s == current.as_os_str()) {
return Err(error_with_path(
io::ErrorKind::InvalidInput,
symlink_path,
"Too many levels of symbolic links",
));
}
visited.push(current.as_os_str().to_os_string());
#[cfg(all(target_os = "linux", feature = "proc-canonicalize"))]
if is_proc_magic_link(¤t) {
break;
}
match fs::read_link(¤t) {
Ok(target) => {
depth += 1;
if depth > effective_max_depth {
return Err(error_with_path(
io::ErrorKind::InvalidInput,
symlink_path,
"Too many levels of symbolic links",
));
}
if target.is_absolute() {
current = target;
} else if let Some(parent) = current.parent() {
current = simple_normalize_path(&parent.join(target));
} else {
current = target;
}
}
Err(_) => break, }
}
Ok(current)
}
#[cfg(all(target_os = "linux", feature = "proc-canonicalize"))]
pub(crate) fn is_proc_magic_link(path: &Path) -> bool {
use std::path::Component;
let comps: Vec<_> = path.components().collect();
if let [Component::RootDir, Component::Normal(proc), Component::Normal(pid), Component::Normal(magic)] =
comps.as_slice()
{
if *proc != "proc" {
return false;
}
if !is_valid_pid_component(pid) {
return false;
}
let magic_str = magic.to_string_lossy();
return matches!(magic_str.as_ref(), "root" | "cwd");
}
if let [Component::RootDir, Component::Normal(proc), Component::Normal(pid), Component::Normal(task), Component::Normal(tid), Component::Normal(magic)] =
comps.as_slice()
{
if *proc != "proc" {
return false;
}
if !is_valid_pid_component(pid) {
return false;
}
if *task != "task" {
return false;
}
if !is_valid_pid_component(tid) {
return false;
}
let magic_str = magic.to_string_lossy();
return matches!(magic_str.as_ref(), "root" | "cwd");
}
false
}
#[cfg(all(target_os = "linux", feature = "proc-canonicalize"))]
#[inline]
fn is_valid_pid_component(s: &std::ffi::OsStr) -> bool {
let s_str = s.to_string_lossy();
s_str == "self" || s_str == "thread-self" || s_str.chars().all(|c| c.is_ascii_digit())
}
#[cfg(target_os = "macos")]
#[inline]
fn is_likely_system_symlink(path: &Path) -> bool {
let s = path.to_string_lossy();
s.starts_with("/var") || s.starts_with("/tmp") || s.starts_with("/etc")
}
#[cfg(target_os = "linux")]
#[inline]
fn is_likely_system_symlink(path: &Path) -> bool {
let s = path.to_string_lossy();
s.starts_with("/lib")
|| s.starts_with("/usr/lib")
|| s.starts_with("/bin")
|| s.starts_with("/sbin")
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
#[inline]
fn is_likely_system_symlink(_path: &Path) -> bool {
false
}
#[cfg(all(test, windows, feature = "anchored"))]
mod clamp_verbatim_regression {
use super::normalize_and_clamp_to_anchor;
use std::path::{Component, Path, Prefix};
fn first_prefix_kind(p: &Path) -> Option<Prefix<'_>> {
p.components().next().and_then(|c| match c {
Component::Prefix(pc) => Some(pc.kind()),
_ => None,
})
}
#[test]
fn verbatim_anchor_and_within_input_yields_verbatim_output() {
let anchor = Path::new(r"\\?\C:\Users\runneradmin\AppData\Local\Temp\.tmpAAAA\home\jail");
let current = Path::new(
r"\\?\C:\Users\runneradmin\AppData\Local\Temp\.tmpAAAA\home\jail\opt\subdir\special",
);
let (clamped, was_clamped) = normalize_and_clamp_to_anchor(current, anchor);
assert!(
!was_clamped,
"input lies within anchor; clamp helper should signal no reclamp"
);
assert!(
matches!(first_prefix_kind(&clamped), Some(Prefix::VerbatimDisk(b'C'))),
"clamped output must preserve VerbatimDisk so stdlib starts_with works; got {:?} for {:?}",
first_prefix_kind(&clamped),
&clamped
);
assert!(
clamped.starts_with(anchor),
"stdlib Path::starts_with must match the anchor; got clamped={:?}, anchor={:?}",
&clamped,
anchor
);
}
}