@@ -2150,6 +2150,13 @@ pub struct Path {
2150
2150
#[stable(since = "1.7.0", feature = "strip_prefix")]
2151
2151
pub struct StripPrefixError(());
2152
2152
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
+
2153
2160
impl Path {
2154
2161
// The following (private!) function allows construction of a path from a u8
2155
2162
// slice, which is only safe when it is known to follow the OsStr encoding.
@@ -2956,6 +2963,63 @@ impl Path {
2956
2963
fs::canonicalize(self)
2957
2964
}
2958
2965
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
+
2959
3023
/// Reads a symbolic link, returning the file that the link points to.
2960
3024
///
2961
3025
/// This is an alias to [`fs::read_link`].
@@ -3497,6 +3561,15 @@ impl Error for StripPrefixError {
3497
3561
}
3498
3562
}
3499
3563
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
+
3500
3573
/// Makes the path absolute without accessing the filesystem.
3501
3574
///
3502
3575
/// If the path is relative, the current directory is used as the base directory.
0 commit comments