use crate::{Error, Result};
use std::{
collections::HashMap,
ffi::OsString,
fs::Metadata,
io,
iter::FusedIterator,
path::{Path, PathBuf},
sync::Arc,
};
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[allow(clippy::exhaustive_enums)]
pub(crate) enum PathType {
Final,
Intermediate,
Symlink,
Content,
}
#[derive(Clone, Debug)]
struct Component {
#[cfg(target_family = "windows")]
is_windows_prefix: bool,
text: OsString,
}
#[cfg(target_family = "windows")]
const INVALID_FUNCTION: i32 = 1;
impl<'a> From<std::path::Component<'a>> for Component {
fn from(c: std::path::Component<'a>) -> Self {
#[cfg(target_family = "windows")]
let is_windows_prefix = matches!(c, std::path::Component::Prefix(_));
let text = c.as_os_str().to_owned();
Component {
#[cfg(target_family = "windows")]
is_windows_prefix,
text,
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct ResolvePath {
resolved: PathBuf,
stack: Vec<Component>,
terminated: bool,
steps_remaining: usize,
already_inspected: HashMap<PathBuf, Option<PathBuf>>,
}
const MAX_STEPS: usize = 1024;
impl ResolvePath {
fn empty() -> Self {
ResolvePath {
resolved: PathBuf::new(),
stack: Vec::new(),
terminated: false,
steps_remaining: MAX_STEPS,
already_inspected: HashMap::new(),
}
}
pub(crate) fn new(path: impl AsRef<Path>) -> Result<Self> {
let mut resolve = Self::empty();
let path = path.as_ref();
push_prefix(&mut resolve.stack, path);
if path.is_relative() {
let cwd = std::env::current_dir().map_err(|e| Error::CurrentDirectory(Arc::new(e)))?;
if !cwd.is_absolute() {
let ioe = io::Error::new(
io::ErrorKind::Other,
format!("Current directory {:?} was not absolute.", cwd),
);
return Err(Error::CurrentDirectory(Arc::new(ioe)));
}
push_prefix(&mut resolve.stack, cwd.as_ref());
}
Ok(resolve)
}
pub(crate) fn into_result(self) -> (PathBuf, Option<PathBuf>) {
let remainder = if self.stack.is_empty() {
None
} else {
Some(self.stack.into_iter().rev().map(|c| c.text).collect())
};
(self.resolved, remainder)
}
}
fn push_prefix(stack: &mut Vec<Component>, path: &Path) {
stack.extend(path.components().rev().map(|component| component.into()));
}
impl Iterator for ResolvePath {
type Item = Result<(PathBuf, PathType, Metadata)>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.terminated {
return None;
}
if self.steps_remaining < self.stack.len() {
self.terminated = true;
return Some(Err(Error::StepsExceeded));
}
let next_part = match self.stack.pop() {
Some(p) => p,
None => {
self.terminated = true;
return None;
}
};
self.steps_remaining -= 1;
let inspecting: std::borrow::Cow<'_, Path> = if next_part.text == "." {
self.resolved.as_path().into()
} else if next_part.text == ".." {
self.resolved
.parent()
.unwrap_or(self.resolved.as_path())
.into()
} else {
self.resolved.join(&next_part.text).into()
};
match self.already_inspected.get(inspecting.as_ref()) {
Some(Some(link_target)) => {
push_prefix(&mut self.stack, link_target.as_path());
continue;
}
Some(None) => {
self.resolved = inspecting.into_owned();
continue;
}
None => {
}
}
let metadata = match inspecting.symlink_metadata() {
Ok(m) => m,
#[cfg(target_family = "windows")]
Err(e)
if next_part.is_windows_prefix
&& e.raw_os_error() == Some(INVALID_FUNCTION) =>
{
self.resolved = inspecting.into_owned();
continue;
}
Err(e) => {
self.stack.push(next_part);
self.terminated = true;
return Some(Err(Error::inspecting(e, inspecting)));
}
};
if metadata.file_type().is_symlink() {
let link_target = match inspecting.read_link() {
Ok(t) => t,
Err(e) => {
self.stack.push(next_part);
self.terminated = true;
return Some(Err(Error::inspecting(e, inspecting)));
}
};
push_prefix(&mut self.stack, link_target.as_path());
self.already_inspected
.insert(inspecting.to_path_buf(), Some(link_target));
return Some(Ok((inspecting.into_owned(), PathType::Symlink, metadata)));
} else {
self.already_inspected
.insert(inspecting.to_path_buf(), None);
self.resolved = inspecting.into_owned();
let path_type = if self.stack.is_empty() {
PathType::Final
} else {
PathType::Intermediate
};
return Some(Ok((self.resolved.clone(), path_type, metadata)));
}
}
}
}
impl FusedIterator for ResolvePath {}
#[cfg(test)]
mod test {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::testing;
#[cfg(target_family = "unix")]
use crate::testing::LinkType;
fn skip_past(r: &mut ResolvePath, p: impl AsRef<Path>) {
#[allow(clippy::manual_flatten)]
for item in r {
if let Ok((name, _, _)) = item {
if name == p.as_ref() {
break;
}
}
}
}
fn make_prefix_verbatim(path: PathBuf) -> PathBuf {
let mut components = path.components();
if let Some(std::path::Component::Prefix(prefix)) = components.next() {
use std::path::Prefix as P;
let verbatim = match prefix.kind() {
P::UNC(server, share) => {
let mut p = OsString::from(r"\\?\UNC\");
p.push(server);
p.push("/");
p.push(share);
p
}
P::Disk(disk) => format!(r"\\?\{}:", disk as char).into(),
_ => return path, };
let mut newpath = PathBuf::from(verbatim);
newpath.extend(components.map(|c| c.as_os_str()));
newpath
} else {
path }
}
#[test]
fn simple_path() {
let d = testing::Dir::new();
let root = d.canonical_root();
d.file("a/b/c");
let mut r = ResolvePath::new(d.path("a/b/c")).unwrap();
skip_past(&mut r, root);
let mut so_far = root.to_path_buf();
for (c, p) in Path::new("a/b/c").components().zip(&mut r) {
let (p, pt, meta) = p.unwrap();
if pt == PathType::Final {
assert_eq!(c.as_os_str(), "c");
assert!(meta.is_file());
} else {
assert_eq!(pt, PathType::Intermediate);
assert!(meta.is_dir());
}
so_far.push(c);
assert_eq!(so_far, p);
}
let (canonical, rest) = r.into_result();
assert_eq!(canonical, d.path("a/b/c").canonicalize().unwrap());
assert!(rest.is_none());
let mut r = ResolvePath::new(d.relative_root().join("a/b/c")).unwrap();
skip_past(&mut r, root);
let mut so_far = root.to_path_buf();
for (c, p) in Path::new("a/b/c").components().zip(&mut r) {
let (p, pt, meta) = p.unwrap();
if pt == PathType::Final {
assert_eq!(c.as_os_str(), "c");
assert!(meta.is_file());
} else {
assert_eq!(pt, PathType::Intermediate);
assert!(meta.is_dir());
}
so_far.push(c);
assert_eq!(so_far, p);
}
let (canonical, rest) = r.into_result();
let canonical = make_prefix_verbatim(canonical);
assert_eq!(canonical, d.path("a/b/c").canonicalize().unwrap());
assert!(rest.is_none());
let mut r = ResolvePath::new(d.path("a/xxx/yyy")).unwrap();
skip_past(&mut r, root);
let (p, pt, _) = r.next().unwrap().unwrap();
assert_eq!(p, root.join("a"));
assert_eq!(pt, PathType::Intermediate);
let e = r.next().unwrap();
match e {
Err(Error::NotFound(p)) => assert_eq!(p, root.join("a/xxx")),
_ => panic!(),
}
let (start, rest) = r.into_result();
assert_eq!(start, d.path("a").canonicalize().unwrap());
assert_eq!(rest.unwrap(), Path::new("xxx/yyy"));
}
#[test]
#[cfg(target_family = "unix")]
fn repeats() {
let d = testing::Dir::new();
let root = d.canonical_root();
d.dir("a/b/c/d");
let mut r = ResolvePath::new(root.join("a/b/../b/../b/c/../c/d")).unwrap();
skip_past(&mut r, root);
let paths: Vec<_> = r.map(|item| item.unwrap().0).collect();
assert_eq!(
paths,
vec![
root.join("a"),
root.join("a/b"),
root.join("a/b/c"),
root.join("a/b/c/d"),
]
);
d.link_rel(LinkType::Dir, "../../", "a/b/c/rel_lnk");
let mut r = ResolvePath::new(root.join("a/b/c/rel_lnk/b/c/d")).unwrap();
skip_past(&mut r, root);
let paths: Vec<_> = r.map(|item| item.unwrap().0).collect();
assert_eq!(
paths,
vec![
root.join("a"),
root.join("a/b"),
root.join("a/b/c"),
root.join("a/b/c/rel_lnk"),
root.join("a/b/c/d"),
]
);
d.link_abs(LinkType::Dir, "a", "a/b/c/abs_lnk");
let mut r = ResolvePath::new(root.join("a/b/c/abs_lnk/b/c/d")).unwrap();
skip_past(&mut r, root);
let paths: Vec<_> = r.map(|item| item.unwrap().0).collect();
assert_eq!(
paths,
vec![
root.join("a"),
root.join("a/b"),
root.join("a/b/c"),
root.join("a/b/c/abs_lnk"),
root.join("a/b/c/d"),
]
);
let mut r = ResolvePath::new(root.join("a/b/c/abs_lnk/b/c/rel_lnk/b/c/d")).unwrap();
skip_past(&mut r, root);
let paths: Vec<_> = r.map(|item| item.unwrap().0).collect();
assert_eq!(
paths,
vec![
root.join("a"),
root.join("a/b"),
root.join("a/b/c"),
root.join("a/b/c/abs_lnk"),
root.join("a/b/c/rel_lnk"),
root.join("a/b/c/d"),
]
);
let mut r =
ResolvePath::new(root.join("a/b/c/abs_lnk/b/c/rel_lnk/b/c/rel_lnk/b/c/abs_lnk/b/c/d"))
.unwrap();
skip_past(&mut r, root);
let paths: Vec<_> = r.map(|item| item.unwrap().0).collect();
assert_eq!(
paths,
vec![
root.join("a"),
root.join("a/b"),
root.join("a/b/c"),
root.join("a/b/c/abs_lnk"),
root.join("a/b/c/rel_lnk"),
root.join("a/b/c/d"),
]
);
}
#[test]
#[cfg(target_family = "unix")]
fn looping() {
let d = testing::Dir::new();
let root = d.canonical_root();
d.dir("a/b/c");
d.link_rel(LinkType::File, "../../b/c/d", "a/b/c/d");
let mut r = ResolvePath::new(root.join("a/b/c/d")).unwrap();
skip_past(&mut r, root);
assert_eq!(r.next().unwrap().unwrap().0, root.join("a"));
assert_eq!(r.next().unwrap().unwrap().0, root.join("a/b"));
assert_eq!(r.next().unwrap().unwrap().0, root.join("a/b/c"));
assert_eq!(r.next().unwrap().unwrap().0, root.join("a/b/c/d"));
assert!(matches!(
r.next().unwrap().unwrap_err(),
Error::StepsExceeded
));
assert!(r.next().is_none());
d.link_rel(LinkType::Dir, "./f", "a/b/c/e");
d.link_rel(LinkType::Dir, "./e", "a/b/c/f");
let mut r = ResolvePath::new(root.join("a/b/c/e/413")).unwrap();
skip_past(&mut r, root);
assert_eq!(r.next().unwrap().unwrap().0, root.join("a"));
assert_eq!(r.next().unwrap().unwrap().0, root.join("a/b"));
assert_eq!(r.next().unwrap().unwrap().0, root.join("a/b/c"));
assert_eq!(r.next().unwrap().unwrap().0, root.join("a/b/c/e"));
assert_eq!(r.next().unwrap().unwrap().0, root.join("a/b/c/f"));
assert!(matches!(
r.next().unwrap().unwrap_err(),
Error::StepsExceeded
));
assert!(r.next().is_none());
}
#[cfg(target_family = "unix")]
#[test]
fn unix_permissions() {
use std::os::unix::prelude::PermissionsExt;
let d = testing::Dir::new();
let root = d.canonical_root();
d.dir("a/b/c/d/e");
d.chmod("a", 0o751);
d.chmod("a/b", 0o711);
d.chmod("a/b/c", 0o715);
d.chmod("a/b/c/d", 0o000);
let mut r = ResolvePath::new(root.join("a/b/c/d/e/413")).unwrap();
skip_past(&mut r, root);
let resolvable: Vec<_> = (&mut r)
.take(4)
.map(|item| {
let (p, _, m) = item.unwrap();
(
p.strip_prefix(root).unwrap().to_string_lossy().into_owned(),
m.permissions().mode() & 0o777,
)
})
.collect();
let expected = vec![
("a", 0o751),
("a/b", 0o711),
("a/b/c", 0o715),
("a/b/c/d", 0o000),
];
for ((p1, m1), (p2, m2)) in resolvable.iter().zip(expected.iter()) {
assert_eq!(p1, p2);
assert_eq!(m1, m2);
}
if unsafe { libc::getuid() } == 0 {
return;
}
let err = r.next().unwrap();
assert!(matches!(err, Err(Error::CouldNotInspect(_, _))));
assert!(r.next().is_none());
}
#[test]
fn past_root() {
let d = testing::Dir::new();
let root = d.canonical_root();
d.dir("a/b");
d.chmod("a", 0o700);
d.chmod("a/b", 0o700);
let root_as_relative: PathBuf = root
.components()
.filter(|c| matches!(c, std::path::Component::Normal(_)))
.collect();
let n = root.components().count();
let mut inspect_path = root.to_path_buf();
for _ in 0..n * 2 {
inspect_path.push("..");
}
inspect_path.push(root_as_relative);
inspect_path.push("a/b");
let r = ResolvePath::new(inspect_path.clone()).unwrap();
let final_path = r.last().unwrap().unwrap().0;
assert_eq!(final_path, inspect_path.canonicalize().unwrap());
}
}