Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add real_clean #10

Merged
merged 10 commits into from
Jun 27, 2024
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# real_parent

Provides a path extension method `real_parent` which is safe in the presence of symlinks.
Provides path extension methods including `real_parent` which are safe in the presence of symlinks.

Noting that `Path::parent` gives incorrect results in the presence of symlinks, `Path::canonicalize` has been used extensively to mitigate this.
This comes, however, with some ergonomic drawbacks (see below).
Expand Down
77 changes: 50 additions & 27 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,45 @@ pub trait PathExt {
/// Any symlink expansion is minimal, that is, as much as possible of the relative and
/// symlinked nature of the receiver is preserved, minimally resolving symlinks are necessary to maintain
/// physical path correctness.
/// For example, no attempt is made to fold away dotdot in the path.
/// For example, no attempt is made to fold away `..` in the path.
///
/// Differences from `Path::parent`
/// - `Path::new("..").parent() == ""`, which is incorrect, so `Path::new("..").real_parent() == "../.."`
/// - `Path::new("foo").parent() == ""`, which is not a valid path, so `Path::new("foo").real_parent() == "."`
/// - where `Path::parent()` returns `None`, `real_parent()` returns self for absolute root path, and appends `..` otherwise
fn real_parent(&self) -> io::Result<PathBuf>;

/// Return a clean path, with `.` and `..` folded away as much as possible, and without expanding symlinks except where required
/// for correctness.
fn real_clean(&self) -> io::Result<PathBuf>;

/// Return whether this is a path to the root directory, regardless of whether or not it is relative or contains symlinks.
/// Empty path is treated as `.`, that is, current directory, for compatibility with `Path::parent`.
fn is_real_root(&self) -> io::Result<bool>;
}

fn empty_to_dot(p: PathBuf) -> PathBuf {
if p.as_os_str().is_empty() {
AsRef::<Path>::as_ref(DOT).to_path_buf()
} else {
p
}
}

impl PathExt for Path {
fn real_parent(&self) -> io::Result<PathBuf> {
let mut real_path = RealPath::default();
real_path
.parent(self)
.map(|p| {
// empty is not a valid path, so we return dot
if p.as_os_str().is_empty() {
AsRef::<Path>::as_ref(DOT).to_path_buf()
} else {
p
}
})
.map(empty_to_dot)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
}

fn real_clean(&self) -> io::Result<PathBuf> {
let mut real_path = RealPath::default();
real_path
.clean(self)
.map(empty_to_dot)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
}

Expand Down Expand Up @@ -101,11 +114,7 @@ impl RealPath {
// unwrap is safe because the last path component is a symlink
let symlink_dir = path.parent().unwrap();

let resolved_target = if target.is_relative() {
self.real_join(symlink_dir, &target)?
} else {
target
};
let resolved_target = self.join(symlink_dir, &target)?;

self.parent(resolved_target.as_path()).map(|p| p.into())
}
Expand All @@ -120,7 +129,7 @@ impl RealPath {
} else {
match path.components().last() {
None | Some(Component::ParentDir) => {
// don't attempt to fold away dotdot in the base path
// don't attempt to fold away `..` in the base path
Ok(path.join(DOTDOT).into())
}
_ => {
Expand All @@ -137,45 +146,59 @@ impl RealPath {
Ok(path.parent().unwrap().into())
}

// join paths
// TODO maybe this should have a public interface
fn real_join<P1, P2>(&mut self, origin: P1, other: P2) -> Result<PathBuf, Error>
// join paths, folding away `..`
fn join<P1, P2>(&mut self, origin: P1, other: P2) -> Result<PathBuf, Error>
where
P1: AsRef<Path>,
P2: AsRef<Path>,
{
let origin = origin.as_ref();
use Component::*;

let other = other.as_ref();

let mut resolving = origin.to_path_buf();
let (root, relative) = other
.components()
.partition::<Vec<_>, _>(|c| matches!(c, Prefix(_) | RootDir));

for component in other.components() {
use Component::*;
let mut resolving = if root.is_empty() {
origin.as_ref().to_path_buf()
} else {
root.iter().collect::<PathBuf>()
};

for component in relative {
match component {
CurDir => (),
Prefix(_) | RootDir => {
panic!(
"impossible absolute component in relative path \"{}\"",
other.to_string_lossy()
"impossible absolute component in relative part of path {:?}",
other
)
}
ParentDir => match self.parent(resolving.as_path()) {
Ok(path) => {
resolving = path.to_path_buf();
resolving = path;
}
Err(e) => {
return Err(e);
}
},
Normal(path_component) => {
resolving.push(path_component);
Normal(_) => {
resolving.push(component);
}
}
}

Ok(resolving)
}

// clean a path, folding away `..`
fn clean<P>(&mut self, path: P) -> Result<PathBuf, Error>
where
P: AsRef<Path>,
{
self.join("", path)
}
}

/// Our internal error type is an io:Error which includes the path which failed, or a cycle error.
Expand Down
Loading