]> 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 64f443a4f794614ba33d7028b901deec8d1bab07..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]
@@ -83,6 +89,10 @@ const (
 // If the result of this process is an empty string, Clean
 // returns the string ".".
 //
+// 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
@@ -91,7 +101,7 @@ func Clean(path string) string {
        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)
                }
@@ -117,21 +127,9 @@ func Clean(path string) string {
                case os.IsPathSeparator(path[r]):
                        // empty path element
                        r++
-               case path[r] == '.' && r+1 == n:
+               case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])):
                        // . element
                        r++
-               case path[r] == '.' && os.IsPathSeparator(path[r+1]):
-                       // ./ element
-                       r++
-
-                       for r < len(path) && os.IsPathSeparator(path[r]) {
-                               r++
-                       }
-                       if out.w == 0 && volumeNameLen(path[r:]) > 0 {
-                               // When joining prefix "." and an absolute path on Windows,
-                               // the prefix should not be removed.
-                               out.append('.')
-                       }
                case path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])):
                        // .. element: remove to last separator
                        r += 2
@@ -169,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.
@@ -409,7 +448,7 @@ func walkDir(path string, d fs.DirEntry, walkDirFn fs.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)
@@ -491,7 +530,7 @@ func WalkDir(root string, fn fs.WalkDirFunc) error {
        if err != nil {
                err = fn(root, nil, err)
        } else {
-               err = walkDir(root, &statDirEntry{info}, fn)
+               err = walkDir(root, fs.FileInfoToDirEntry(info), fn)
        }
        if err == SkipDir || err == SkipAll {
                return nil
@@ -499,15 +538,6 @@ func WalkDir(root string, fn fs.WalkDirFunc) error {
        return err
 }
 
-type statDirEntry struct {
-       info fs.FileInfo
-}
-
-func (d *statDirEntry) Name() string               { return d.info.Name() }
-func (d *statDirEntry) IsDir() bool                { return d.info.IsDir() }
-func (d *statDirEntry) Type() fs.FileMode          { return d.info.Mode().Type() }
-func (d *statDirEntry) Info() (fs.FileInfo, error) { return d.info, nil }
-
 // Walk walks the file tree rooted at root, calling fn for each file or
 // directory in the tree, including root.
 //
@@ -535,22 +565,6 @@ func Walk(root string, fn WalkFunc) error {
        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) {
@@ -621,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)])
 }