@@ -2154,6 +2154,13 @@ pub struct Path {
21542154#[ stable( since = "1.7.0" , feature = "strip_prefix" ) ]
21552155pub struct StripPrefixError ( ( ) ) ;
21562156
2157+ /// An error returned from [`Path::normalize_lexically`] if a `..` parent reference
2158+ /// would escape the path.
2159+ #[ unstable( feature = "normalize_lexically" , issue = "134694" ) ]
2160+ #[ derive( Debug , PartialEq ) ]
2161+ #[ non_exhaustive]
2162+ pub struct NormalizeError ;
2163+
21572164impl Path {
21582165 // The following (private!) function allows construction of a path from a u8
21592166 // slice, which is only safe when it is known to follow the OsStr encoding.
@@ -2961,6 +2968,67 @@ impl Path {
29612968 fs:: canonicalize ( self )
29622969 }
29632970
2971+ /// Normalize a path, including `..` without traversing the filesystem.
2972+ ///
2973+ /// Returns an error if normalization would leave leading `..` components.
2974+ ///
2975+ /// <div class="warning">
2976+ ///
2977+ /// This function always resolves `..` to the "lexical" parent.
2978+ /// That is "a/b/../c" will always resolve to `a/c` which can change the meaning of the path.
2979+ /// 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`.
2980+ ///
2981+ /// </div>
2982+ ///
2983+ /// [`path::absolute`](absolute) is an alternative that preserves `..`.
2984+ /// Or [`Path::canonicalize`] can be used to resolve any `..` by querying the filesystem.
2985+ #[ unstable( feature = "normalize_lexically" , issue = "134694" ) ]
2986+ pub fn normalize_lexically ( & self ) -> Result < PathBuf , NormalizeError > {
2987+ let mut lexical = PathBuf :: new ( ) ;
2988+ let mut iter = self . components ( ) . peekable ( ) ;
2989+
2990+ // Find the root, if any, and add it to the lexical path.
2991+ // Here we treat the Windows path "C:\" as a single "root" even though
2992+ // `components` splits it into two: (Prefix, RootDir).
2993+ let root = match iter. peek ( ) {
2994+ Some ( Component :: ParentDir ) => return Err ( NormalizeError ) ,
2995+ Some ( p @ Component :: RootDir ) | Some ( p @ Component :: CurDir ) => {
2996+ lexical. push ( p) ;
2997+ iter. next ( ) ;
2998+ lexical. as_os_str ( ) . len ( )
2999+ }
3000+ Some ( Component :: Prefix ( prefix) ) => {
3001+ lexical. push ( prefix. as_os_str ( ) ) ;
3002+ iter. next ( ) ;
3003+ if let Some ( p @ Component :: RootDir ) = iter. peek ( ) {
3004+ lexical. push ( p) ;
3005+ iter. next ( ) ;
3006+ }
3007+ lexical. as_os_str ( ) . len ( )
3008+ }
3009+ None => return Ok ( PathBuf :: new ( ) ) ,
3010+ Some ( Component :: Normal ( _) ) => 0 ,
3011+ } ;
3012+
3013+ for component in iter {
3014+ match component {
3015+ Component :: RootDir => unreachable ! ( ) ,
3016+ Component :: Prefix ( _) => return Err ( NormalizeError ) ,
3017+ Component :: CurDir => continue ,
3018+ Component :: ParentDir => {
3019+ // It's an error if ParentDir causes us to go above the "root".
3020+ if lexical. as_os_str ( ) . len ( ) == root {
3021+ return Err ( NormalizeError ) ;
3022+ } else {
3023+ lexical. pop ( ) ;
3024+ }
3025+ }
3026+ Component :: Normal ( path) => lexical. push ( path) ,
3027+ }
3028+ }
3029+ Ok ( lexical)
3030+ }
3031+
29643032 /// Reads a symbolic link, returning the file that the link points to.
29653033 ///
29663034 /// This is an alias to [`fs::read_link`].
@@ -3502,6 +3570,15 @@ impl Error for StripPrefixError {
35023570 }
35033571}
35043572
3573+ #[ unstable( feature = "normalize_lexically" , issue = "134694" ) ]
3574+ impl fmt:: Display for NormalizeError {
3575+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
3576+ f. write_str ( "parent reference `..` points outside of base directory" )
3577+ }
3578+ }
3579+ #[ unstable( feature = "normalize_lexically" , issue = "134694" ) ]
3580+ impl Error for NormalizeError { }
3581+
35053582/// Makes the path absolute without accessing the filesystem.
35063583///
35073584/// If the path is relative, the current directory is used as the base directory.
0 commit comments