Skip to content

Commit

Permalink
Add real_clean (#10)
Browse files Browse the repository at this point in the history
Also refactor the test helpers to make them a bit cleaner.
  • Loading branch information
tesujimath authored Jun 27, 2024
1 parent f775350 commit e9420ae
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 139 deletions.
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

0 comments on commit e9420ae

Please sign in to comment.