]> Cypherpunks.ru repositories - gostls13.git/blobdiff - src/path/filepath/path.go
path/filepath: fix various issues in parsing Windows paths
[gostls13.git] / src / path / filepath / path.go
index 3f7e5c713dc47ebce6e55d57aeb525effcdbc8cb..3d693f840a9ef5fe569283360283115db2dc14f9 100644 (file)
@@ -8,13 +8,14 @@
 // The filepath package uses either forward slashes or backslashes,
 // depending on the operating system. To process paths such as URLs
 // that always use forward slashes regardless of the operating
-// system, see the path package.
+// system, see the [path] package.
 package filepath
 
 import (
        "errors"
        "io/fs"
        "os"
+       "slices"
        "sort"
        "strings"
 )
@@ -51,6 +52,11 @@ func (b *lazybuf) append(c byte) {
        b.w++
 }
 
+func (b *lazybuf) prepend(prefix ...byte) {
+       b.buf = slices.Insert(b.buf, 0, prefix...)
+       b.w += len(prefix)
+}
+
 func (b *lazybuf) string() string {
        if b.buf == nil {
                return b.volAndPath[:b.volLen+b.w]
@@ -67,13 +73,13 @@ const (
 // by purely lexical processing. It applies the following rules
 // iteratively until no further processing can be done:
 //
-//     1. Replace multiple Separator elements with a single one.
-//     2. Eliminate each . path name element (the current directory).
-//     3. Eliminate each inner .. path name element (the parent directory)
-//        along with the non-.. element that precedes it.
-//     4. Eliminate .. elements that begin a rooted path:
-//        that is, replace "/.." by "/" at the beginning of a path,
-//        assuming Separator is '/'.
+//  1. Replace multiple Separator elements with a single one.
+//  2. Eliminate each . path name element (the current directory).
+//  3. Eliminate each inner .. path name element (the parent directory)
+//     along with the non-.. element that precedes it.
+//  4. Eliminate .. elements that begin a rooted path:
+//     that is, replace "/.." by "/" at the beginning of a path,
+//     assuming Separator is '/'.
 //
 // The returned path ends in a slash only if it represents a root directory,
 // such as "/" on Unix or `C:\` on Windows.
@@ -83,15 +89,19 @@ const (
 // If the result of this process is an empty string, Clean
 // returns the string ".".
 //
-// See also Rob Pike, ``Lexical File Names in Plan 9 or
-// Getting Dot-Dot Right,''
+// On Windows, Clean does not modify the volume name other than to replace
+// occurrences of "/" with `\`.
+// For example, Clean("//host/share/../x") returns `\\host\share\x`.
+//
+// See also Rob Pike, “Lexical File Names in Plan 9 or
+// Getting Dot-Dot Right,”
 // https://9p.io/sys/doc/lexnames.html
 func Clean(path string) string {
        originalPath := path
        volLen := volumeNameLen(path)
        path = path[volLen:]
        if path == "" {
-               if volLen > 1 && originalPath[1] != ':' {
+               if volLen > 1 && os.IsPathSeparator(originalPath[0]) && os.IsPathSeparator(originalPath[1]) {
                        // should be UNC
                        return FromSlash(originalPath)
                }
@@ -157,9 +167,50 @@ func Clean(path string) string {
                out.append('.')
        }
 
+       postClean(&out) // avoid creating absolute paths on Windows
        return FromSlash(out.string())
 }
 
+// IsLocal reports whether path, using lexical analysis only, has all of these properties:
+//
+//   - is within the subtree rooted at the directory in which path is evaluated
+//   - is not an absolute path
+//   - is not empty
+//   - on Windows, is not a reserved name such as "NUL"
+//
+// If IsLocal(path) returns true, then
+// Join(base, path) will always produce a path contained within base and
+// Clean(path) will always produce an unrooted path with no ".." path elements.
+//
+// IsLocal is a purely lexical operation.
+// In particular, it does not account for the effect of any symbolic links
+// that may exist in the filesystem.
+func IsLocal(path string) bool {
+       return isLocal(path)
+}
+
+func unixIsLocal(path string) bool {
+       if IsAbs(path) || path == "" {
+               return false
+       }
+       hasDots := false
+       for p := path; p != ""; {
+               var part string
+               part, p, _ = strings.Cut(p, "/")
+               if part == "." || part == ".." {
+                       hasDots = true
+                       break
+               }
+       }
+       if hasDots {
+               path = Clean(path)
+       }
+       if path == ".." || strings.HasPrefix(path, "../") {
+               return false
+       }
+       return true
+}
+
 // ToSlash returns the result of replacing each separator character
 // in path with a slash ('/') character. Multiple separators are
 // replaced by multiple slashes.
@@ -275,7 +326,11 @@ func Rel(basepath, targpath string) (string, error) {
        targ = targ[len(targVol):]
        if base == "." {
                base = ""
+       } else if base == "" && volumeNameLen(baseVol) > 2 /* isUNC */ {
+               // Treat any targetpath matching `\\host\share` basepath as absolute path.
+               base = string(Separator)
        }
+
        // Can't use IsAbs - `\a` and `a` are both relative in Windows.
        baseSlashed := len(base) > 0 && base[0] == Separator
        targSlashed := len(targ) > 0 && targ[0] == Separator
@@ -334,61 +389,14 @@ func Rel(basepath, targpath string) (string, error) {
 // SkipDir is used as a return value from WalkFuncs to indicate that
 // the directory named in the call is to be skipped. It is not returned
 // as an error by any function.
-var SkipDir = errors.New("skip this directory")
+var SkipDir error = fs.SkipDir
 
-// WalkDirFunc is the type of the function called by WalkDir to visit
-// each each file or directory.
-//
-// The path argument contains the argument to Walk as a prefix.
-// That is, if Walk is called with root argument "dir" and finds a file
-// named "a" in that directory, the walk function will be called with
-// argument "dir/a".
-//
-// The directory and file are joined with Join, which may clean the
-// directory name: if Walk is called with the root argument "x/../dir"
-// and finds a file named "a" in that directory, the walk function will
-// be called with argument "dir/a", not "x/../dir/a".
-//
-// The d argument is the fs.DirEntry for the named path.
-//
-// The error result returned by the function controls how WalkDir
-// continues. If the function returns the special value SkipDir, WalkDir
-// skips the current directory (path if d.IsDir() is true, otherwise
-// path's parent directory). Otherwise, if the function returns a non-nil
-// error, WalkDir stops entirely and returns that error.
-//
-// The err argument reports an error related to path, signaling that
-// WalkDir will not walk into that directory. The function can decide how
-// to handle that error; as described earlier, returning the error will
-// cause WalkDir to stop walking the entire tree.
-//
-// WalkDir calls the function with a non-nil err argument in two cases.
-//
-// First, if the initial os.Lstat on the root directory fails, WalkDir
-// calls the function with path set to root, d set to nil, and err set to
-// the error from os.Lstat.
-//
-// Second, if a directory's ReadDir method fails, WalkDir calls the
-// function with path set to the directory's path, d set to an
-// fs.DirEntry describing the directory, and err set to the error from
-// ReadDir. In this second case, the function is called twice with the
-// path of the directory: the first call is before the directory read is
-// attempted and has err set to nil, giving the function a chance to
-// return SkipDir and avoid the ReadDir entirely. The second call is
-// after a failed ReadDir and reports the error from ReadDir.
-// (If ReadDir succeeds, there is no second call.)
-//
-// The differences between WalkDirFunc compared to WalkFunc are:
-//
-//   - The second argument has type fs.DirEntry instead of fs.FileInfo.
-//   - The function is called before reading a directory, to allow SkipDir
-//     to bypass the directory read entirely.
-//   - If a directory read fails, the function is called a second time
-//     for that directory to report the error.
-//
-type WalkDirFunc func(path string, d fs.DirEntry, err error) error
+// SkipAll is used as a return value from WalkFuncs to indicate that
+// all remaining files and directories are to be skipped. It is not returned
+// as an error by any function.
+var SkipAll error = fs.SkipAll
 
-// WalkFunc is the type of the function called by Walk to visit each each
+// WalkFunc is the type of the function called by Walk to visit each
 // file or directory.
 //
 // The path argument contains the argument to Walk as a prefix.
@@ -406,8 +414,9 @@ type WalkDirFunc func(path string, d fs.DirEntry, err error) error
 // The error result returned by the function controls how Walk continues.
 // If the function returns the special value SkipDir, Walk skips the
 // current directory (path if info.IsDir() is true, otherwise path's
-// parent directory). Otherwise, if the function returns a non-nil error,
-// Walk stops entirely and returns that error.
+// parent directory). If the function returns the special value SkipAll,
+// Walk skips all remaining files and directories. Otherwise, if the function
+// returns a non-nil error, Walk stops entirely and returns that error.
 //
 // The err argument reports an error related to path, signaling that Walk
 // will not walk into that directory. The function can decide how to
@@ -430,7 +439,7 @@ type WalkFunc func(path string, info fs.FileInfo, err error) error
 var lstat = os.Lstat // for testing
 
 // walkDir recursively descends path, calling walkDirFn.
-func walkDir(path string, d fs.DirEntry, walkDirFn WalkDirFunc) error {
+func walkDir(path string, d fs.DirEntry, walkDirFn fs.WalkDirFunc) error {
        if err := walkDirFn(path, d, nil); err != nil || !d.IsDir() {
                if err == SkipDir && d.IsDir() {
                        // Successfully skipped directory.
@@ -439,11 +448,14 @@ func walkDir(path string, d fs.DirEntry, walkDirFn WalkDirFunc) error {
                return err
        }
 
-       dirs, err := readDir(path)
+       dirs, err := os.ReadDir(path)
        if err != nil {
                // Second call, to report ReadDir error.
                err = walkDirFn(path, d, err)
                if err != nil {
+                       if err == SkipDir && d.IsDir() {
+                               err = nil
+                       }
                        return err
                }
        }
@@ -474,7 +486,7 @@ func walk(path string, info fs.FileInfo, walkFn WalkFunc) error {
        if err != nil || err1 != nil {
                // The caller's behavior is controlled by the return value, which is decided
                // by walkFn. walkFn may ignore err and return nil.
-               // If walkFn returns SkipDir, it will be handled by the caller.
+               // If walkFn returns SkipDir or SkipAll, it will be handled by the caller.
                // So walk should return whatever walkFn returns.
                return err1
        }
@@ -502,38 +514,30 @@ func walk(path string, info fs.FileInfo, walkFn WalkFunc) error {
 // directory in the tree, including root.
 //
 // All errors that arise visiting files and directories are filtered by fn:
-// see the WalkDirFunc documentation for details.
+// see the fs.WalkDirFunc documentation for details.
 //
 // The files are walked in lexical order, which makes the output deterministic
 // but requires WalkDir to read an entire directory into memory before proceeding
 // to walk that directory.
 //
 // WalkDir does not follow symbolic links.
-func WalkDir(root string, fn WalkDirFunc) error {
+//
+// WalkDir calls fn with paths that use the separator character appropriate
+// for the operating system. This is unlike [io/fs.WalkDir], which always
+// uses slash separated paths.
+func WalkDir(root string, fn fs.WalkDirFunc) error {
        info, err := os.Lstat(root)
        if err != nil {
                err = fn(root, nil, err)
        } else {
-               err = walkDir(root, &dirEntryFromInfo{info}, fn)
+               err = walkDir(root, fs.FileInfoToDirEntry(info), fn)
        }
-       if err == SkipDir {
+       if err == SkipDir || err == SkipAll {
                return nil
        }
        return err
 }
 
-type dirEntryFromInfo struct {
-       fs.FileInfo
-}
-
-func (e *dirEntryFromInfo) Type() fs.FileMode {
-       return e.Mode().Type()
-}
-
-func (e *dirEntryFromInfo) Info() (fs.FileInfo, error) {
-       return e.FileInfo, nil
-}
-
 // Walk walks the file tree rooted at root, calling fn for each file or
 // directory in the tree, including root.
 //
@@ -555,28 +559,12 @@ func Walk(root string, fn WalkFunc) error {
        } else {
                err = walk(root, info, fn)
        }
-       if err == SkipDir {
+       if err == SkipDir || err == SkipAll {
                return nil
        }
        return err
 }
 
-// readDir reads the directory named by dirname and returns
-// a sorted list of directory entries.
-func readDir(dirname string) ([]fs.DirEntry, error) {
-       f, err := os.Open(dirname)
-       if err != nil {
-               return nil, err
-       }
-       dirs, err := f.ReadDir(-1)
-       f.Close()
-       if err != nil {
-               return nil, err
-       }
-       sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
-       return dirs, nil
-}
-
 // readDirNames reads the directory named by dirname and returns
 // a sorted list of directory entry names.
 func readDirNames(dirname string) ([]string, error) {
@@ -647,5 +635,5 @@ func Dir(path string) string {
 // Given "\\host\share\foo" it returns "\\host\share".
 // On other platforms it returns "".
 func VolumeName(path string) string {
-       return path[:volumeNameLen(path)]
+       return FromSlash(path[:volumeNameLen(path)])
 }