@@ -2150,6 +2150,13 @@ pub struct Path {
21502150#[ stable( since = "1.7.0" , feature = "strip_prefix" ) ]
21512151pub 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+
21532160impl 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.
0 commit comments