Implement normalize lexically

This commit is contained in:
Chris Denton
2024-12-23 15:09:51 +00:00
parent 3d86494a0d
commit c299e297ee
2 changed files with 132 additions and 1 deletions

View File

@@ -2154,6 +2154,13 @@ pub struct Path {
#[stable(since = "1.7.0", feature = "strip_prefix")]
pub struct StripPrefixError(());
/// An error returned from [`Path::normalize_lexically`] if a `..` parent reference
/// would escape the path.
#[unstable(feature = "normalize_lexically", issue = "134694")]
#[derive(Debug, PartialEq)]
#[non_exhaustive]
pub struct NormalizeError;
impl Path {
// The following (private!) function allows construction of a path from a u8
// slice, which is only safe when it is known to follow the OsStr encoding.
@@ -2961,6 +2968,67 @@ impl Path {
fs::canonicalize(self)
}
/// Normalize a path, including `..` without traversing the filesystem.
///
/// Returns an error if normalization would leave leading `..` components.
///
/// <div class="warning">
///
/// This function always resolves `..` to the "lexical" parent.
/// That is "a/b/../c" will always resolve to `a/c` which can change the meaning of the path.
/// In particular, `a/c` and `a/b/../c` are distinct on many systems because `b` may be a symbolic link, so its parent isnt `a`.
///
/// </div>
///
/// [`path::absolute`](absolute) is an alternative that preserves `..`.
/// Or [`Path::canonicalize`] can be used to resolve any `..` by querying the filesystem.
#[unstable(feature = "normalize_lexically", issue = "134694")]
pub fn normalize_lexically(&self) -> Result<PathBuf, NormalizeError> {
let mut lexical = PathBuf::new();
let mut iter = self.components().peekable();
// Find the root, if any, and add it to the lexical path.
// Here we treat the Windows path "C:\" as a single "root" even though
// `components` splits it into two: (Prefix, RootDir).
let root = match iter.peek() {
Some(Component::ParentDir) => return Err(NormalizeError),
Some(p @ Component::RootDir) | Some(p @ Component::CurDir) => {
lexical.push(p);
iter.next();
lexical.as_os_str().len()
}
Some(Component::Prefix(prefix)) => {
lexical.push(prefix.as_os_str());
iter.next();
if let Some(p @ Component::RootDir) = iter.peek() {
lexical.push(p);
iter.next();
}
lexical.as_os_str().len()
}
None => return Ok(PathBuf::new()),
Some(Component::Normal(_)) => 0,
};
for component in iter {
match component {
Component::RootDir => unreachable!(),
Component::Prefix(_) => return Err(NormalizeError),
Component::CurDir => continue,
Component::ParentDir => {
// It's an error if ParentDir causes us to go above the "root".
if lexical.as_os_str().len() == root {
return Err(NormalizeError);
} else {
lexical.pop();
}
}
Component::Normal(path) => lexical.push(path),
}
}
Ok(lexical)
}
/// Reads a symbolic link, returning the file that the link points to.
///
/// This is an alias to [`fs::read_link`].
@@ -3502,6 +3570,15 @@ impl Error for StripPrefixError {
}
}
#[unstable(feature = "normalize_lexically", issue = "134694")]
impl fmt::Display for NormalizeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("parent reference `..` points outside of base directory")
}
}
#[unstable(feature = "normalize_lexically", issue = "134694")]
impl Error for NormalizeError {}
/// Makes the path absolute without accessing the filesystem.
///
/// If the path is relative, the current directory is used as the base directory.