Skip to content

Commit 2955213

Browse files
ChrisDentonworkingjubilee
authored andcommitted
Implement normalize lexically
1 parent 3350c1e commit 2955213

File tree

2 files changed

+121
-0
lines changed

2 files changed

+121
-0
lines changed

library/std/src/path.rs

+73
Original file line numberDiff line numberDiff line change
@@ -2150,6 +2150,13 @@ pub struct Path {
21502150
#[stable(since = "1.7.0", feature = "strip_prefix")]
21512151
pub struct StripPrefixError(());
21522152

2153+
/// An error returned from [`Path::normalize_lexically`] if a `..` parent reference
2154+
/// would escape the path.
2155+
#[unstable(feature = "normalize_lexically", issue = "134694")]
2156+
#[derive(Debug, PartialEq)]
2157+
#[non_exhaustive]
2158+
pub struct NormalizeError;
2159+
21532160
impl Path {
21542161
// The following (private!) function allows construction of a path from a u8
21552162
// slice, which is only safe when it is known to follow the OsStr encoding.
@@ -2956,6 +2963,63 @@ impl Path {
29562963
fs::canonicalize(self)
29572964
}
29582965

2966+
/// Normalize a path, including `..` without traversing the filesystem.
2967+
///
2968+
/// Returns an error if normalization would leave leading `..` components.
2969+
///
2970+
/// <div class="warning">
2971+
///
2972+
/// This function always resolves `..` to the "lexical" parent.
2973+
/// That is "a/b/../c" will always resolve to `a/c` which can change the meaning of the path.
2974+
/// In particular, `a/c` and `a/b/../c` are distinct on many systems because `b` may be a symbolic link, so its parent isn’t `a`.
2975+
///
2976+
/// </div>
2977+
///
2978+
/// [`path::absolute`](absolute) is an alternative that preserves `..`.
2979+
/// Or [`Path::canonicalize`] can be used to resolve any `..` by querying the filesystem.
2980+
#[unstable(feature = "normalize_lexically", issue = "134694")]
2981+
pub fn normalize_lexically(&self) -> Result<PathBuf, NormalizeError> {
2982+
let mut lexical = PathBuf::new();
2983+
let mut iter = self.components().peekable();
2984+
2985+
// Find the root, if any.
2986+
let root = match iter.peek() {
2987+
Some(Component::ParentDir) => return Err(NormalizeError),
2988+
Some(p @ Component::RootDir) | Some(p @ Component::CurDir) => {
2989+
lexical.push(p);
2990+
iter.next();
2991+
lexical.as_os_str().len()
2992+
}
2993+
Some(Component::Prefix(prefix)) => {
2994+
lexical.push(prefix.as_os_str());
2995+
iter.next();
2996+
if let Some(p @ Component::RootDir) = iter.peek() {
2997+
lexical.push(p);
2998+
iter.next();
2999+
}
3000+
lexical.as_os_str().len()
3001+
}
3002+
None => return Ok(PathBuf::new()),
3003+
Some(Component::Normal(_)) => 0,
3004+
};
3005+
3006+
for component in iter {
3007+
match component {
3008+
Component::RootDir | Component::Prefix(_) => return Err(NormalizeError),
3009+
Component::CurDir => continue,
3010+
Component::ParentDir => {
3011+
if lexical.as_os_str().len() == root {
3012+
return Err(NormalizeError);
3013+
} else {
3014+
lexical.pop();
3015+
}
3016+
}
3017+
Component::Normal(path) => lexical.push(path),
3018+
}
3019+
}
3020+
Ok(lexical)
3021+
}
3022+
29593023
/// Reads a symbolic link, returning the file that the link points to.
29603024
///
29613025
/// This is an alias to [`fs::read_link`].
@@ -3497,6 +3561,15 @@ impl Error for StripPrefixError {
34973561
}
34983562
}
34993563

3564+
#[unstable(feature = "normalize_lexically", issue = "134694")]
3565+
impl fmt::Display for NormalizeError {
3566+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3567+
f.write_str("parent reference `..` points outside of base directory")
3568+
}
3569+
}
3570+
#[unstable(feature = "normalize_lexically", issue = "134694")]
3571+
impl Error for NormalizeError {}
3572+
35003573
/// Makes the path absolute without accessing the filesystem.
35013574
///
35023575
/// If the path is relative, the current directory is used as the base directory.

library/std/tests/path.rs

+48
Original file line numberDiff line numberDiff line change
@@ -1976,3 +1976,51 @@ fn clone_to_uninit() {
19761976
unsafe { a.clone_to_uninit(ptr::from_mut::<Path>(&mut b).cast()) };
19771977
assert_eq!(a, &*b);
19781978
}
1979+
1980+
#[test]
1981+
fn normalize_lexically() {
1982+
#[track_caller]
1983+
fn check(a: &str, b: Result<&str, NormalizeError>) {
1984+
assert_eq!(Path::new(a).normalize_lexically(), b.map(PathBuf::from));
1985+
}
1986+
1987+
// Relative paths
1988+
check("a", Ok("a"));
1989+
check("./a", Ok("./a"));
1990+
check("a/b/c", Ok("a/b/c"));
1991+
check("a/././b/./c/.", Ok("a/b/c"));
1992+
check("a/../c", Ok("c"));
1993+
check("./a/b", Ok("./a/b"));
1994+
check("a/../b/c/..", Ok("b"));
1995+
1996+
check("..", Err(NormalizeError));
1997+
check("../..", Err(NormalizeError));
1998+
check("a/../..", Err(NormalizeError));
1999+
check("a/../../b", Err(NormalizeError));
2000+
check("a/../../b/c", Err(NormalizeError));
2001+
check("a/../b/../..", Err(NormalizeError));
2002+
2003+
// Check we don't escape the root or prefix
2004+
#[cfg(unix)]
2005+
{
2006+
check("/..", Err(NormalizeError));
2007+
check("/a/../..", Err(NormalizeError));
2008+
}
2009+
#[cfg(windows)]
2010+
{
2011+
check(r"C:\..", Err(NormalizeError));
2012+
check(r"C:\a\..\..", Err(NormalizeError));
2013+
2014+
check(r"C:..", Err(NormalizeError));
2015+
check(r"C:a\..\..", Err(NormalizeError));
2016+
2017+
check(r"\\server\share\..", Err(NormalizeError));
2018+
check(r"\\server\share\a\..\..", Err(NormalizeError));
2019+
2020+
check(r"\..", Err(NormalizeError));
2021+
check(r"\a\..\..", Err(NormalizeError));
2022+
2023+
check(r"\\?\UNC\server\share\..", Err(NormalizeError));
2024+
check(r"\\?\UNC\server\share\a\..\..", Err(NormalizeError));
2025+
}
2026+
}

0 commit comments

Comments
 (0)