diff options
Diffstat (limited to 'src/path/filepath')
-rw-r--r-- | src/path/filepath/example_test.go | 20 | ||||
-rw-r--r-- | src/path/filepath/example_unix_test.go | 171 | ||||
-rw-r--r-- | src/path/filepath/example_unix_walk_test.go | 66 | ||||
-rw-r--r-- | src/path/filepath/export_test.go | 7 | ||||
-rw-r--r-- | src/path/filepath/export_windows_test.go | 10 | ||||
-rw-r--r-- | src/path/filepath/match.go | 369 | ||||
-rw-r--r-- | src/path/filepath/match_test.go | 383 | ||||
-rw-r--r-- | src/path/filepath/path.go | 667 | ||||
-rw-r--r-- | src/path/filepath/path_nonwindows.go | 9 | ||||
-rw-r--r-- | src/path/filepath/path_plan9.go | 55 | ||||
-rw-r--r-- | src/path/filepath/path_test.go | 1925 | ||||
-rw-r--r-- | src/path/filepath/path_unix.go | 57 | ||||
-rw-r--r-- | src/path/filepath/path_windows.go | 348 | ||||
-rw-r--r-- | src/path/filepath/path_windows_test.go | 588 | ||||
-rw-r--r-- | src/path/filepath/symlink.go | 149 | ||||
-rw-r--r-- | src/path/filepath/symlink_plan9.go | 27 | ||||
-rw-r--r-- | src/path/filepath/symlink_unix.go | 11 | ||||
-rw-r--r-- | src/path/filepath/symlink_windows.go | 118 |
18 files changed, 4980 insertions, 0 deletions
diff --git a/src/path/filepath/example_test.go b/src/path/filepath/example_test.go new file mode 100644 index 0000000..a1d680e --- /dev/null +++ b/src/path/filepath/example_test.go @@ -0,0 +1,20 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filepath_test + +import ( + "fmt" + "path/filepath" +) + +func ExampleExt() { + fmt.Printf("No dots: %q\n", filepath.Ext("index")) + fmt.Printf("One dot: %q\n", filepath.Ext("index.js")) + fmt.Printf("Two dots: %q\n", filepath.Ext("main.test.js")) + // Output: + // No dots: "" + // One dot: ".js" + // Two dots: ".js" +} diff --git a/src/path/filepath/example_unix_test.go b/src/path/filepath/example_unix_test.go new file mode 100644 index 0000000..b364cf0 --- /dev/null +++ b/src/path/filepath/example_unix_test.go @@ -0,0 +1,171 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !windows && !plan9 + +package filepath_test + +import ( + "fmt" + "path/filepath" +) + +func ExampleSplitList() { + fmt.Println("On Unix:", filepath.SplitList("/a/b/c:/usr/bin")) + // Output: + // On Unix: [/a/b/c /usr/bin] +} + +func ExampleRel() { + paths := []string{ + "/a/b/c", + "/b/c", + "./b/c", + } + base := "/a" + + fmt.Println("On Unix:") + for _, p := range paths { + rel, err := filepath.Rel(base, p) + fmt.Printf("%q: %q %v\n", p, rel, err) + } + + // Output: + // On Unix: + // "/a/b/c": "b/c" <nil> + // "/b/c": "../b/c" <nil> + // "./b/c": "" Rel: can't make ./b/c relative to /a +} + +func ExampleSplit() { + paths := []string{ + "/home/arnie/amelia.jpg", + "/mnt/photos/", + "rabbit.jpg", + "/usr/local//go", + } + fmt.Println("On Unix:") + for _, p := range paths { + dir, file := filepath.Split(p) + fmt.Printf("input: %q\n\tdir: %q\n\tfile: %q\n", p, dir, file) + } + // Output: + // On Unix: + // input: "/home/arnie/amelia.jpg" + // dir: "/home/arnie/" + // file: "amelia.jpg" + // input: "/mnt/photos/" + // dir: "/mnt/photos/" + // file: "" + // input: "rabbit.jpg" + // dir: "" + // file: "rabbit.jpg" + // input: "/usr/local//go" + // dir: "/usr/local//" + // file: "go" +} + +func ExampleJoin() { + fmt.Println("On Unix:") + fmt.Println(filepath.Join("a", "b", "c")) + fmt.Println(filepath.Join("a", "b/c")) + fmt.Println(filepath.Join("a/b", "c")) + fmt.Println(filepath.Join("a/b", "/c")) + + fmt.Println(filepath.Join("a/b", "../../../xyz")) + + // Output: + // On Unix: + // a/b/c + // a/b/c + // a/b/c + // a/b/c + // ../xyz +} + +func ExampleMatch() { + fmt.Println("On Unix:") + fmt.Println(filepath.Match("/home/catch/*", "/home/catch/foo")) + fmt.Println(filepath.Match("/home/catch/*", "/home/catch/foo/bar")) + fmt.Println(filepath.Match("/home/?opher", "/home/gopher")) + fmt.Println(filepath.Match("/home/\\*", "/home/*")) + + // Output: + // On Unix: + // true <nil> + // false <nil> + // true <nil> + // true <nil> +} + +func ExampleBase() { + fmt.Println("On Unix:") + fmt.Println(filepath.Base("/foo/bar/baz.js")) + fmt.Println(filepath.Base("/foo/bar/baz")) + fmt.Println(filepath.Base("/foo/bar/baz/")) + fmt.Println(filepath.Base("dev.txt")) + fmt.Println(filepath.Base("../todo.txt")) + fmt.Println(filepath.Base("..")) + fmt.Println(filepath.Base(".")) + fmt.Println(filepath.Base("/")) + fmt.Println(filepath.Base("")) + + // Output: + // On Unix: + // baz.js + // baz + // baz + // dev.txt + // todo.txt + // .. + // . + // / + // . +} + +func ExampleDir() { + fmt.Println("On Unix:") + fmt.Println(filepath.Dir("/foo/bar/baz.js")) + fmt.Println(filepath.Dir("/foo/bar/baz")) + fmt.Println(filepath.Dir("/foo/bar/baz/")) + fmt.Println(filepath.Dir("/dirty//path///")) + fmt.Println(filepath.Dir("dev.txt")) + fmt.Println(filepath.Dir("../todo.txt")) + fmt.Println(filepath.Dir("..")) + fmt.Println(filepath.Dir(".")) + fmt.Println(filepath.Dir("/")) + fmt.Println(filepath.Dir("")) + + // Output: + // On Unix: + // /foo/bar + // /foo/bar + // /foo/bar/baz + // /dirty/path + // . + // .. + // . + // . + // / + // . +} + +func ExampleIsAbs() { + fmt.Println("On Unix:") + fmt.Println(filepath.IsAbs("/home/gopher")) + fmt.Println(filepath.IsAbs(".bashrc")) + fmt.Println(filepath.IsAbs("..")) + fmt.Println(filepath.IsAbs(".")) + fmt.Println(filepath.IsAbs("/")) + fmt.Println(filepath.IsAbs("")) + + // Output: + // On Unix: + // true + // false + // false + // false + // true + // false +} diff --git a/src/path/filepath/example_unix_walk_test.go b/src/path/filepath/example_unix_walk_test.go new file mode 100644 index 0000000..86146db --- /dev/null +++ b/src/path/filepath/example_unix_walk_test.go @@ -0,0 +1,66 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !windows && !plan9 + +package filepath_test + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" +) + +func prepareTestDirTree(tree string) (string, error) { + tmpDir, err := os.MkdirTemp("", "") + if err != nil { + return "", fmt.Errorf("error creating temp directory: %v\n", err) + } + + err = os.MkdirAll(filepath.Join(tmpDir, tree), 0755) + if err != nil { + os.RemoveAll(tmpDir) + return "", err + } + + return tmpDir, nil +} + +func ExampleWalk() { + tmpDir, err := prepareTestDirTree("dir/to/walk/skip") + if err != nil { + fmt.Printf("unable to create test dir tree: %v\n", err) + return + } + defer os.RemoveAll(tmpDir) + os.Chdir(tmpDir) + + subDirToSkip := "skip" + + fmt.Println("On Unix:") + err = filepath.Walk(".", func(path string, info fs.FileInfo, err error) error { + if err != nil { + fmt.Printf("prevent panic by handling failure accessing a path %q: %v\n", path, err) + return err + } + if info.IsDir() && info.Name() == subDirToSkip { + fmt.Printf("skipping a dir without errors: %+v \n", info.Name()) + return filepath.SkipDir + } + fmt.Printf("visited file or dir: %q\n", path) + return nil + }) + if err != nil { + fmt.Printf("error walking the path %q: %v\n", tmpDir, err) + return + } + // Output: + // On Unix: + // visited file or dir: "." + // visited file or dir: "dir" + // visited file or dir: "dir/to" + // visited file or dir: "dir/to/walk" + // skipping a dir without errors: skip +} diff --git a/src/path/filepath/export_test.go b/src/path/filepath/export_test.go new file mode 100644 index 0000000..0cf9e3b --- /dev/null +++ b/src/path/filepath/export_test.go @@ -0,0 +1,7 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filepath + +var LstatP = &lstat diff --git a/src/path/filepath/export_windows_test.go b/src/path/filepath/export_windows_test.go new file mode 100644 index 0000000..a7e2e64 --- /dev/null +++ b/src/path/filepath/export_windows_test.go @@ -0,0 +1,10 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filepath + +var ( + ToNorm = toNorm + NormBase = normBase +) diff --git a/src/path/filepath/match.go b/src/path/filepath/match.go new file mode 100644 index 0000000..b5cc4b8 --- /dev/null +++ b/src/path/filepath/match.go @@ -0,0 +1,369 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filepath + +import ( + "errors" + "os" + "runtime" + "sort" + "strings" + "unicode/utf8" +) + +// ErrBadPattern indicates a pattern was malformed. +var ErrBadPattern = errors.New("syntax error in pattern") + +// Match reports whether name matches the shell file name pattern. +// The pattern syntax is: +// +// pattern: +// { term } +// term: +// '*' matches any sequence of non-Separator characters +// '?' matches any single non-Separator character +// '[' [ '^' ] { character-range } ']' +// character class (must be non-empty) +// c matches character c (c != '*', '?', '\\', '[') +// '\\' c matches character c +// +// character-range: +// c matches character c (c != '\\', '-', ']') +// '\\' c matches character c +// lo '-' hi matches character c for lo <= c <= hi +// +// Match requires pattern to match all of name, not just a substring. +// The only possible returned error is ErrBadPattern, when pattern +// is malformed. +// +// On Windows, escaping is disabled. Instead, '\\' is treated as +// path separator. +func Match(pattern, name string) (matched bool, err error) { +Pattern: + for len(pattern) > 0 { + var star bool + var chunk string + star, chunk, pattern = scanChunk(pattern) + if star && chunk == "" { + // Trailing * matches rest of string unless it has a /. + return !strings.Contains(name, string(Separator)), nil + } + // Look for match at current position. + t, ok, err := matchChunk(chunk, name) + // if we're the last chunk, make sure we've exhausted the name + // otherwise we'll give a false result even if we could still match + // using the star + if ok && (len(t) == 0 || len(pattern) > 0) { + name = t + continue + } + if err != nil { + return false, err + } + if star { + // Look for match skipping i+1 bytes. + // Cannot skip /. + for i := 0; i < len(name) && name[i] != Separator; i++ { + t, ok, err := matchChunk(chunk, name[i+1:]) + if ok { + // if we're the last chunk, make sure we exhausted the name + if len(pattern) == 0 && len(t) > 0 { + continue + } + name = t + continue Pattern + } + if err != nil { + return false, err + } + } + } + return false, nil + } + return len(name) == 0, nil +} + +// scanChunk gets the next segment of pattern, which is a non-star string +// possibly preceded by a star. +func scanChunk(pattern string) (star bool, chunk, rest string) { + for len(pattern) > 0 && pattern[0] == '*' { + pattern = pattern[1:] + star = true + } + inrange := false + var i int +Scan: + for i = 0; i < len(pattern); i++ { + switch pattern[i] { + case '\\': + if runtime.GOOS != "windows" { + // error check handled in matchChunk: bad pattern. + if i+1 < len(pattern) { + i++ + } + } + case '[': + inrange = true + case ']': + inrange = false + case '*': + if !inrange { + break Scan + } + } + } + return star, pattern[0:i], pattern[i:] +} + +// matchChunk checks whether chunk matches the beginning of s. +// If so, it returns the remainder of s (after the match). +// Chunk is all single-character operators: literals, char classes, and ?. +func matchChunk(chunk, s string) (rest string, ok bool, err error) { + // failed records whether the match has failed. + // After the match fails, the loop continues on processing chunk, + // checking that the pattern is well-formed but no longer reading s. + failed := false + for len(chunk) > 0 { + if !failed && len(s) == 0 { + failed = true + } + switch chunk[0] { + case '[': + // character class + var r rune + if !failed { + var n int + r, n = utf8.DecodeRuneInString(s) + s = s[n:] + } + chunk = chunk[1:] + // possibly negated + negated := false + if len(chunk) > 0 && chunk[0] == '^' { + negated = true + chunk = chunk[1:] + } + // parse all ranges + match := false + nrange := 0 + for { + if len(chunk) > 0 && chunk[0] == ']' && nrange > 0 { + chunk = chunk[1:] + break + } + var lo, hi rune + if lo, chunk, err = getEsc(chunk); err != nil { + return "", false, err + } + hi = lo + if chunk[0] == '-' { + if hi, chunk, err = getEsc(chunk[1:]); err != nil { + return "", false, err + } + } + if lo <= r && r <= hi { + match = true + } + nrange++ + } + if match == negated { + failed = true + } + + case '?': + if !failed { + if s[0] == Separator { + failed = true + } + _, n := utf8.DecodeRuneInString(s) + s = s[n:] + } + chunk = chunk[1:] + + case '\\': + if runtime.GOOS != "windows" { + chunk = chunk[1:] + if len(chunk) == 0 { + return "", false, ErrBadPattern + } + } + fallthrough + + default: + if !failed { + if chunk[0] != s[0] { + failed = true + } + s = s[1:] + } + chunk = chunk[1:] + } + } + if failed { + return "", false, nil + } + return s, true, nil +} + +// getEsc gets a possibly-escaped character from chunk, for a character class. +func getEsc(chunk string) (r rune, nchunk string, err error) { + if len(chunk) == 0 || chunk[0] == '-' || chunk[0] == ']' { + err = ErrBadPattern + return + } + if chunk[0] == '\\' && runtime.GOOS != "windows" { + chunk = chunk[1:] + if len(chunk) == 0 { + err = ErrBadPattern + return + } + } + r, n := utf8.DecodeRuneInString(chunk) + if r == utf8.RuneError && n == 1 { + err = ErrBadPattern + } + nchunk = chunk[n:] + if len(nchunk) == 0 { + err = ErrBadPattern + } + return +} + +// Glob returns the names of all files matching pattern or nil +// if there is no matching file. The syntax of patterns is the same +// as in Match. The pattern may describe hierarchical names such as +// /usr/*/bin/ed (assuming the Separator is '/'). +// +// Glob ignores file system errors such as I/O errors reading directories. +// The only possible returned error is ErrBadPattern, when pattern +// is malformed. +func Glob(pattern string) (matches []string, err error) { + return globWithLimit(pattern, 0) +} + +func globWithLimit(pattern string, depth int) (matches []string, err error) { + // This limit is used prevent stack exhaustion issues. See CVE-2022-30632. + const pathSeparatorsLimit = 10000 + if depth == pathSeparatorsLimit { + return nil, ErrBadPattern + } + + // Check pattern is well-formed. + if _, err := Match(pattern, ""); err != nil { + return nil, err + } + if !hasMeta(pattern) { + if _, err = os.Lstat(pattern); err != nil { + return nil, nil + } + return []string{pattern}, nil + } + + dir, file := Split(pattern) + volumeLen := 0 + if runtime.GOOS == "windows" { + volumeLen, dir = cleanGlobPathWindows(dir) + } else { + dir = cleanGlobPath(dir) + } + + if !hasMeta(dir[volumeLen:]) { + return glob(dir, file, nil) + } + + // Prevent infinite recursion. See issue 15879. + if dir == pattern { + return nil, ErrBadPattern + } + + var m []string + m, err = globWithLimit(dir, depth+1) + if err != nil { + return + } + for _, d := range m { + matches, err = glob(d, file, matches) + if err != nil { + return + } + } + return +} + +// cleanGlobPath prepares path for glob matching. +func cleanGlobPath(path string) string { + switch path { + case "": + return "." + case string(Separator): + // do nothing to the path + return path + default: + return path[0 : len(path)-1] // chop off trailing separator + } +} + +// cleanGlobPathWindows is windows version of cleanGlobPath. +func cleanGlobPathWindows(path string) (prefixLen int, cleaned string) { + vollen := volumeNameLen(path) + switch { + case path == "": + return 0, "." + case vollen+1 == len(path) && os.IsPathSeparator(path[len(path)-1]): // /, \, C:\ and C:/ + // do nothing to the path + return vollen + 1, path + case vollen == len(path) && len(path) == 2: // C: + return vollen, path + "." // convert C: into C:. + default: + if vollen >= len(path) { + vollen = len(path) - 1 + } + return vollen, path[0 : len(path)-1] // chop off trailing separator + } +} + +// glob searches for files matching pattern in the directory dir +// and appends them to matches. If the directory cannot be +// opened, it returns the existing matches. New matches are +// added in lexicographical order. +func glob(dir, pattern string, matches []string) (m []string, e error) { + m = matches + fi, err := os.Stat(dir) + if err != nil { + return // ignore I/O error + } + if !fi.IsDir() { + return // ignore I/O error + } + d, err := os.Open(dir) + if err != nil { + return // ignore I/O error + } + defer d.Close() + + names, _ := d.Readdirnames(-1) + sort.Strings(names) + + for _, n := range names { + matched, err := Match(pattern, n) + if err != nil { + return m, err + } + if matched { + m = append(m, Join(dir, n)) + } + } + return +} + +// hasMeta reports whether path contains any of the magic characters +// recognized by Match. +func hasMeta(path string) bool { + magicChars := `*?[` + if runtime.GOOS != "windows" { + magicChars = `*?[\` + } + return strings.ContainsAny(path, magicChars) +} diff --git a/src/path/filepath/match_test.go b/src/path/filepath/match_test.go new file mode 100644 index 0000000..d628259 --- /dev/null +++ b/src/path/filepath/match_test.go @@ -0,0 +1,383 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filepath_test + +import ( + "fmt" + "internal/testenv" + "os" + . "path/filepath" + "reflect" + "runtime" + "sort" + "strings" + "testing" +) + +type MatchTest struct { + pattern, s string + match bool + err error +} + +var matchTests = []MatchTest{ + {"abc", "abc", true, nil}, + {"*", "abc", true, nil}, + {"*c", "abc", true, nil}, + {"a*", "a", true, nil}, + {"a*", "abc", true, nil}, + {"a*", "ab/c", false, nil}, + {"a*/b", "abc/b", true, nil}, + {"a*/b", "a/c/b", false, nil}, + {"a*b*c*d*e*/f", "axbxcxdxe/f", true, nil}, + {"a*b*c*d*e*/f", "axbxcxdxexxx/f", true, nil}, + {"a*b*c*d*e*/f", "axbxcxdxe/xxx/f", false, nil}, + {"a*b*c*d*e*/f", "axbxcxdxexxx/fff", false, nil}, + {"a*b?c*x", "abxbbxdbxebxczzx", true, nil}, + {"a*b?c*x", "abxbbxdbxebxczzy", false, nil}, + {"ab[c]", "abc", true, nil}, + {"ab[b-d]", "abc", true, nil}, + {"ab[e-g]", "abc", false, nil}, + {"ab[^c]", "abc", false, nil}, + {"ab[^b-d]", "abc", false, nil}, + {"ab[^e-g]", "abc", true, nil}, + {"a\\*b", "a*b", true, nil}, + {"a\\*b", "ab", false, nil}, + {"a?b", "a☺b", true, nil}, + {"a[^a]b", "a☺b", true, nil}, + {"a???b", "a☺b", false, nil}, + {"a[^a][^a][^a]b", "a☺b", false, nil}, + {"[a-ζ]*", "α", true, nil}, + {"*[a-ζ]", "A", false, nil}, + {"a?b", "a/b", false, nil}, + {"a*b", "a/b", false, nil}, + {"[\\]a]", "]", true, nil}, + {"[\\-]", "-", true, nil}, + {"[x\\-]", "x", true, nil}, + {"[x\\-]", "-", true, nil}, + {"[x\\-]", "z", false, nil}, + {"[\\-x]", "x", true, nil}, + {"[\\-x]", "-", true, nil}, + {"[\\-x]", "a", false, nil}, + {"[]a]", "]", false, ErrBadPattern}, + {"[-]", "-", false, ErrBadPattern}, + {"[x-]", "x", false, ErrBadPattern}, + {"[x-]", "-", false, ErrBadPattern}, + {"[x-]", "z", false, ErrBadPattern}, + {"[-x]", "x", false, ErrBadPattern}, + {"[-x]", "-", false, ErrBadPattern}, + {"[-x]", "a", false, ErrBadPattern}, + {"\\", "a", false, ErrBadPattern}, + {"[a-b-c]", "a", false, ErrBadPattern}, + {"[", "a", false, ErrBadPattern}, + {"[^", "a", false, ErrBadPattern}, + {"[^bc", "a", false, ErrBadPattern}, + {"a[", "a", false, ErrBadPattern}, + {"a[", "ab", false, ErrBadPattern}, + {"a[", "x", false, ErrBadPattern}, + {"a/b[", "x", false, ErrBadPattern}, + {"*x", "xxx", true, nil}, +} + +func errp(e error) string { + if e == nil { + return "<nil>" + } + return e.Error() +} + +func TestMatch(t *testing.T) { + for _, tt := range matchTests { + pattern := tt.pattern + s := tt.s + if runtime.GOOS == "windows" { + if strings.Contains(pattern, "\\") { + // no escape allowed on windows. + continue + } + pattern = Clean(pattern) + s = Clean(s) + } + ok, err := Match(pattern, s) + if ok != tt.match || err != tt.err { + t.Errorf("Match(%#q, %#q) = %v, %q want %v, %q", pattern, s, ok, errp(err), tt.match, errp(tt.err)) + } + } +} + +// contains reports whether vector contains the string s. +func contains(vector []string, s string) bool { + for _, elem := range vector { + if elem == s { + return true + } + } + return false +} + +var globTests = []struct { + pattern, result string +}{ + {"match.go", "match.go"}, + {"mat?h.go", "match.go"}, + {"*", "match.go"}, + {"../*/match.go", "../filepath/match.go"}, +} + +func TestGlob(t *testing.T) { + for _, tt := range globTests { + pattern := tt.pattern + result := tt.result + if runtime.GOOS == "windows" { + pattern = Clean(pattern) + result = Clean(result) + } + matches, err := Glob(pattern) + if err != nil { + t.Errorf("Glob error for %q: %s", pattern, err) + continue + } + if !contains(matches, result) { + t.Errorf("Glob(%#q) = %#v want %v", pattern, matches, result) + } + } + for _, pattern := range []string{"no_match", "../*/no_match"} { + matches, err := Glob(pattern) + if err != nil { + t.Errorf("Glob error for %q: %s", pattern, err) + continue + } + if len(matches) != 0 { + t.Errorf("Glob(%#q) = %#v want []", pattern, matches) + } + } +} + +func TestCVE202230632(t *testing.T) { + // Prior to CVE-2022-30632, this would cause a stack exhaustion given a + // large number of separators (more than 4,000,000). There is now a limit + // of 10,000. + _, err := Glob("/*" + strings.Repeat("/", 10001)) + if err != ErrBadPattern { + t.Fatalf("Glob returned err=%v, want ErrBadPattern", err) + } +} + +func TestGlobError(t *testing.T) { + bad := []string{`[]`, `nonexist/[]`} + for _, pattern := range bad { + if _, err := Glob(pattern); err != ErrBadPattern { + t.Errorf("Glob(%#q) returned err=%v, want ErrBadPattern", pattern, err) + } + } +} + +func TestGlobUNC(t *testing.T) { + // Just make sure this runs without crashing for now. + // See issue 15879. + Glob(`\\?\C:\*`) +} + +var globSymlinkTests = []struct { + path, dest string + brokenLink bool +}{ + {"test1", "link1", false}, + {"test2", "link2", true}, +} + +func TestGlobSymlink(t *testing.T) { + testenv.MustHaveSymlink(t) + + tmpDir := t.TempDir() + for _, tt := range globSymlinkTests { + path := Join(tmpDir, tt.path) + dest := Join(tmpDir, tt.dest) + f, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + err = os.Symlink(path, dest) + if err != nil { + t.Fatal(err) + } + if tt.brokenLink { + // Break the symlink. + os.Remove(path) + } + matches, err := Glob(dest) + if err != nil { + t.Errorf("GlobSymlink error for %q: %s", dest, err) + } + if !contains(matches, dest) { + t.Errorf("Glob(%#q) = %#v want %v", dest, matches, dest) + } + } +} + +type globTest struct { + pattern string + matches []string +} + +func (test *globTest) buildWant(root string) []string { + want := make([]string, 0) + for _, m := range test.matches { + want = append(want, root+FromSlash(m)) + } + sort.Strings(want) + return want +} + +func (test *globTest) globAbs(root, rootPattern string) error { + p := FromSlash(rootPattern + `\` + test.pattern) + have, err := Glob(p) + if err != nil { + return err + } + sort.Strings(have) + want := test.buildWant(root + `\`) + if strings.Join(want, "_") == strings.Join(have, "_") { + return nil + } + return fmt.Errorf("Glob(%q) returns %q, but %q expected", p, have, want) +} + +func (test *globTest) globRel(root string) error { + p := root + FromSlash(test.pattern) + have, err := Glob(p) + if err != nil { + return err + } + sort.Strings(have) + want := test.buildWant(root) + if strings.Join(want, "_") == strings.Join(have, "_") { + return nil + } + // try also matching version without root prefix + wantWithNoRoot := test.buildWant("") + if strings.Join(wantWithNoRoot, "_") == strings.Join(have, "_") { + return nil + } + return fmt.Errorf("Glob(%q) returns %q, but %q expected", p, have, want) +} + +func TestWindowsGlob(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skipf("skipping windows specific test") + } + + tmpDir := tempDirCanonical(t) + if len(tmpDir) < 3 { + t.Fatalf("tmpDir path %q is too short", tmpDir) + } + if tmpDir[1] != ':' { + t.Fatalf("tmpDir path %q must have drive letter in it", tmpDir) + } + + dirs := []string{ + "a", + "b", + "dir/d/bin", + } + files := []string{ + "dir/d/bin/git.exe", + } + for _, dir := range dirs { + err := os.MkdirAll(Join(tmpDir, dir), 0777) + if err != nil { + t.Fatal(err) + } + } + for _, file := range files { + err := os.WriteFile(Join(tmpDir, file), nil, 0666) + if err != nil { + t.Fatal(err) + } + } + + tests := []globTest{ + {"a", []string{"a"}}, + {"b", []string{"b"}}, + {"c", []string{}}, + {"*", []string{"a", "b", "dir"}}, + {"d*", []string{"dir"}}, + {"*i*", []string{"dir"}}, + {"*r", []string{"dir"}}, + {"?ir", []string{"dir"}}, + {"?r", []string{}}, + {"d*/*/bin/git.exe", []string{"dir/d/bin/git.exe"}}, + } + + // test absolute paths + for _, test := range tests { + var p string + if err := test.globAbs(tmpDir, tmpDir); err != nil { + t.Error(err) + } + // test C:\*Documents and Settings\... + p = tmpDir + p = strings.Replace(p, `:\`, `:\*`, 1) + if err := test.globAbs(tmpDir, p); err != nil { + t.Error(err) + } + // test C:\Documents and Settings*\... + p = tmpDir + p = strings.Replace(p, `:\`, `:`, 1) + p = strings.Replace(p, `\`, `*\`, 1) + p = strings.Replace(p, `:`, `:\`, 1) + if err := test.globAbs(tmpDir, p); err != nil { + t.Error(err) + } + } + + // test relative paths + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + err = os.Chdir(tmpDir) + if err != nil { + t.Fatal(err) + } + defer func() { + err := os.Chdir(wd) + if err != nil { + t.Fatal(err) + } + }() + for _, test := range tests { + err := test.globRel("") + if err != nil { + t.Error(err) + } + err = test.globRel(`.\`) + if err != nil { + t.Error(err) + } + err = test.globRel(tmpDir[:2]) // C: + if err != nil { + t.Error(err) + } + } +} + +func TestNonWindowsGlobEscape(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("skipping non-windows specific test") + } + pattern := `\match.go` + want := []string{"match.go"} + matches, err := Glob(pattern) + if err != nil { + t.Fatalf("Glob error for %q: %s", pattern, err) + } + if !reflect.DeepEqual(matches, want) { + t.Fatalf("Glob(%#q) = %v want %v", pattern, matches, want) + } +} diff --git a/src/path/filepath/path.go b/src/path/filepath/path.go new file mode 100644 index 0000000..3bf3ff6 --- /dev/null +++ b/src/path/filepath/path.go @@ -0,0 +1,667 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package filepath implements utility routines for manipulating filename paths +// in a way compatible with the target operating system-defined file paths. +// +// 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. +package filepath + +import ( + "errors" + "io/fs" + "os" + "sort" + "strings" +) + +// A lazybuf is a lazily constructed path buffer. +// It supports append, reading previously appended bytes, +// and retrieving the final string. It does not allocate a buffer +// to hold the output until that output diverges from s. +type lazybuf struct { + path string + buf []byte + w int + volAndPath string + volLen int +} + +func (b *lazybuf) index(i int) byte { + if b.buf != nil { + return b.buf[i] + } + return b.path[i] +} + +func (b *lazybuf) append(c byte) { + if b.buf == nil { + if b.w < len(b.path) && b.path[b.w] == c { + b.w++ + return + } + b.buf = make([]byte, len(b.path)) + copy(b.buf, b.path[:b.w]) + } + b.buf[b.w] = c + b.w++ +} + +func (b *lazybuf) prepend(prefix ...byte) { + b.buf = append(prefix, b.buf...) + b.w += len(prefix) +} + +func (b *lazybuf) string() string { + if b.buf == nil { + return b.volAndPath[:b.volLen+b.w] + } + return b.volAndPath[:b.volLen] + string(b.buf[:b.w]) +} + +const ( + Separator = os.PathSeparator + ListSeparator = os.PathListSeparator +) + +// Clean returns the shortest path name equivalent to path +// 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 '/'. +// +// The returned path ends in a slash only if it represents a root directory, +// such as "/" on Unix or `C:\` on Windows. +// +// Finally, any occurrences of slash are replaced by Separator. +// +// 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 +func Clean(path string) string { + originalPath := path + volLen := volumeNameLen(path) + path = path[volLen:] + if path == "" { + if volLen > 1 && os.IsPathSeparator(originalPath[0]) && os.IsPathSeparator(originalPath[1]) { + // should be UNC + return FromSlash(originalPath) + } + return originalPath + "." + } + rooted := os.IsPathSeparator(path[0]) + + // Invariants: + // reading from path; r is index of next byte to process. + // writing to buf; w is index of next byte to write. + // dotdot is index in buf where .. must stop, either because + // it is the leading slash or it is a leading ../../.. prefix. + n := len(path) + out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen} + r, dotdot := 0, 0 + if rooted { + out.append(Separator) + r, dotdot = 1, 1 + } + + for r < n { + switch { + case os.IsPathSeparator(path[r]): + // empty path element + r++ + case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])): + // . element + r++ + case path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])): + // .. element: remove to last separator + r += 2 + switch { + case out.w > dotdot: + // can backtrack + out.w-- + for out.w > dotdot && !os.IsPathSeparator(out.index(out.w)) { + out.w-- + } + case !rooted: + // cannot backtrack, but not rooted, so append .. element. + if out.w > 0 { + out.append(Separator) + } + out.append('.') + out.append('.') + dotdot = out.w + } + default: + // real path element. + // add slash if needed + if rooted && out.w != 1 || !rooted && out.w != 0 { + out.append(Separator) + } + // copy element + for ; r < n && !os.IsPathSeparator(path[r]); r++ { + out.append(path[r]) + } + } + } + + // Turn empty string into "." + if out.w == 0 { + 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. +func ToSlash(path string) string { + if Separator == '/' { + return path + } + return strings.ReplaceAll(path, string(Separator), "/") +} + +// FromSlash returns the result of replacing each slash ('/') character +// in path with a separator character. Multiple slashes are replaced +// by multiple separators. +func FromSlash(path string) string { + if Separator == '/' { + return path + } + return strings.ReplaceAll(path, "/", string(Separator)) +} + +// SplitList splits a list of paths joined by the OS-specific ListSeparator, +// usually found in PATH or GOPATH environment variables. +// Unlike strings.Split, SplitList returns an empty slice when passed an empty +// string. +func SplitList(path string) []string { + return splitList(path) +} + +// Split splits path immediately following the final Separator, +// separating it into a directory and file name component. +// If there is no Separator in path, Split returns an empty dir +// and file set to path. +// The returned values have the property that path = dir+file. +func Split(path string) (dir, file string) { + vol := VolumeName(path) + i := len(path) - 1 + for i >= len(vol) && !os.IsPathSeparator(path[i]) { + i-- + } + return path[:i+1], path[i+1:] +} + +// Join joins any number of path elements into a single path, +// separating them with an OS specific Separator. Empty elements +// are ignored. The result is Cleaned. However, if the argument +// list is empty or all its elements are empty, Join returns +// an empty string. +// On Windows, the result will only be a UNC path if the first +// non-empty element is a UNC path. +func Join(elem ...string) string { + return join(elem) +} + +// Ext returns the file name extension used by path. +// The extension is the suffix beginning at the final dot +// in the final element of path; it is empty if there is +// no dot. +func Ext(path string) string { + for i := len(path) - 1; i >= 0 && !os.IsPathSeparator(path[i]); i-- { + if path[i] == '.' { + return path[i:] + } + } + return "" +} + +// EvalSymlinks returns the path name after the evaluation of any symbolic +// links. +// If path is relative the result will be relative to the current directory, +// unless one of the components is an absolute symbolic link. +// EvalSymlinks calls Clean on the result. +func EvalSymlinks(path string) (string, error) { + return evalSymlinks(path) +} + +// Abs returns an absolute representation of path. +// If the path is not absolute it will be joined with the current +// working directory to turn it into an absolute path. The absolute +// path name for a given file is not guaranteed to be unique. +// Abs calls Clean on the result. +func Abs(path string) (string, error) { + return abs(path) +} + +func unixAbs(path string) (string, error) { + if IsAbs(path) { + return Clean(path), nil + } + wd, err := os.Getwd() + if err != nil { + return "", err + } + return Join(wd, path), nil +} + +// Rel returns a relative path that is lexically equivalent to targpath when +// joined to basepath with an intervening separator. That is, +// Join(basepath, Rel(basepath, targpath)) is equivalent to targpath itself. +// On success, the returned path will always be relative to basepath, +// even if basepath and targpath share no elements. +// An error is returned if targpath can't be made relative to basepath or if +// knowing the current working directory would be necessary to compute it. +// Rel calls Clean on the result. +func Rel(basepath, targpath string) (string, error) { + baseVol := VolumeName(basepath) + targVol := VolumeName(targpath) + base := Clean(basepath) + targ := Clean(targpath) + if sameWord(targ, base) { + return ".", nil + } + base = base[len(baseVol):] + 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 + if baseSlashed != targSlashed || !sameWord(baseVol, targVol) { + return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath) + } + // Position base[b0:bi] and targ[t0:ti] at the first differing elements. + bl := len(base) + tl := len(targ) + var b0, bi, t0, ti int + for { + for bi < bl && base[bi] != Separator { + bi++ + } + for ti < tl && targ[ti] != Separator { + ti++ + } + if !sameWord(targ[t0:ti], base[b0:bi]) { + break + } + if bi < bl { + bi++ + } + if ti < tl { + ti++ + } + b0 = bi + t0 = ti + } + if base[b0:bi] == ".." { + return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath) + } + if b0 != bl { + // Base elements left. Must go up before going down. + seps := strings.Count(base[b0:bl], string(Separator)) + size := 2 + seps*3 + if tl != t0 { + size += 1 + tl - t0 + } + buf := make([]byte, size) + n := copy(buf, "..") + for i := 0; i < seps; i++ { + buf[n] = Separator + copy(buf[n+1:], "..") + n += 3 + } + if t0 != tl { + buf[n] = Separator + copy(buf[n+1:], targ[t0:]) + } + return string(buf), nil + } + return targ[t0:], nil +} + +// 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 error = fs.SkipDir + +// 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 +// 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 info argument is the fs.FileInfo for the named path. +// +// 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). 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 +// handle that error; as described earlier, returning the error will +// cause Walk to stop walking the entire tree. +// +// Walk calls the function with a non-nil err argument in two cases. +// +// First, if an os.Lstat on the root directory or any directory or file +// in the tree fails, Walk calls the function with path set to that +// directory or file's path, info set to nil, and err set to the error +// from os.Lstat. +// +// Second, if a directory's Readdirnames method fails, Walk calls the +// function with path set to the directory's path, info, set to an +// fs.FileInfo describing the directory, and err set to the error from +// Readdirnames. +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 fs.WalkDirFunc) error { + if err := walkDirFn(path, d, nil); err != nil || !d.IsDir() { + if err == SkipDir && d.IsDir() { + // Successfully skipped directory. + err = nil + } + return err + } + + dirs, err := 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 + } + } + + for _, d1 := range dirs { + path1 := Join(path, d1.Name()) + if err := walkDir(path1, d1, walkDirFn); err != nil { + if err == SkipDir { + break + } + return err + } + } + return nil +} + +// walk recursively descends path, calling walkFn. +func walk(path string, info fs.FileInfo, walkFn WalkFunc) error { + if !info.IsDir() { + return walkFn(path, info, nil) + } + + names, err := readDirNames(path) + err1 := walkFn(path, info, err) + // If err != nil, walk can't walk into this directory. + // err1 != nil means walkFn want walk to skip this directory or stop walking. + // Therefore, if one of err and err1 isn't nil, walk will return. + 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 or SkipAll, it will be handled by the caller. + // So walk should return whatever walkFn returns. + return err1 + } + + for _, name := range names { + filename := Join(path, name) + fileInfo, err := lstat(filename) + if err != nil { + if err := walkFn(filename, fileInfo, err); err != nil && err != SkipDir { + return err + } + } else { + err = walk(filename, fileInfo, walkFn) + if err != nil { + if !fileInfo.IsDir() || err != SkipDir { + return err + } + } + } + } + return nil +} + +// WalkDir walks the file tree rooted at root, calling fn for each file or +// directory in the tree, including root. +// +// All errors that arise visiting files and directories are filtered by fn: +// 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. +// +// 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, &statDirEntry{info}, fn) + } + if err == SkipDir || err == SkipAll { + return nil + } + 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 } + +func (d *statDirEntry) String() string { + return fs.FormatDirEntry(d) +} + +// Walk walks the file tree rooted at root, calling fn for each file or +// directory in the tree, including root. +// +// All errors that arise visiting files and directories are filtered by fn: +// see the WalkFunc documentation for details. +// +// The files are walked in lexical order, which makes the output deterministic +// but requires Walk to read an entire directory into memory before proceeding +// to walk that directory. +// +// Walk does not follow symbolic links. +// +// Walk is less efficient than WalkDir, introduced in Go 1.16, +// which avoids calling os.Lstat on every visited file or directory. +func Walk(root string, fn WalkFunc) error { + info, err := os.Lstat(root) + if err != nil { + err = fn(root, nil, err) + } else { + err = walk(root, info, fn) + } + 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) { + f, err := os.Open(dirname) + if err != nil { + return nil, err + } + names, err := f.Readdirnames(-1) + f.Close() + if err != nil { + return nil, err + } + sort.Strings(names) + return names, nil +} + +// Base returns the last element of path. +// Trailing path separators are removed before extracting the last element. +// If the path is empty, Base returns ".". +// If the path consists entirely of separators, Base returns a single separator. +func Base(path string) string { + if path == "" { + return "." + } + // Strip trailing slashes. + for len(path) > 0 && os.IsPathSeparator(path[len(path)-1]) { + path = path[0 : len(path)-1] + } + // Throw away volume name + path = path[len(VolumeName(path)):] + // Find the last element + i := len(path) - 1 + for i >= 0 && !os.IsPathSeparator(path[i]) { + i-- + } + if i >= 0 { + path = path[i+1:] + } + // If empty now, it had only slashes. + if path == "" { + return string(Separator) + } + return path +} + +// Dir returns all but the last element of path, typically the path's directory. +// After dropping the final element, Dir calls Clean on the path and trailing +// slashes are removed. +// If the path is empty, Dir returns ".". +// If the path consists entirely of separators, Dir returns a single separator. +// The returned path does not end in a separator unless it is the root directory. +func Dir(path string) string { + vol := VolumeName(path) + i := len(path) - 1 + for i >= len(vol) && !os.IsPathSeparator(path[i]) { + i-- + } + dir := Clean(path[len(vol) : i+1]) + if dir == "." && len(vol) > 2 { + // must be UNC + return vol + } + return vol + dir +} + +// VolumeName returns leading volume name. +// Given "C:\foo\bar" it returns "C:" on Windows. +// Given "\\host\share\foo" it returns "\\host\share". +// On other platforms it returns "". +func VolumeName(path string) string { + return FromSlash(path[:volumeNameLen(path)]) +} diff --git a/src/path/filepath/path_nonwindows.go b/src/path/filepath/path_nonwindows.go new file mode 100644 index 0000000..db69f02 --- /dev/null +++ b/src/path/filepath/path_nonwindows.go @@ -0,0 +1,9 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !windows + +package filepath + +func postClean(out *lazybuf) {} diff --git a/src/path/filepath/path_plan9.go b/src/path/filepath/path_plan9.go new file mode 100644 index 0000000..453206a --- /dev/null +++ b/src/path/filepath/path_plan9.go @@ -0,0 +1,55 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filepath + +import "strings" + +func isLocal(path string) bool { + return unixIsLocal(path) +} + +// IsAbs reports whether the path is absolute. +func IsAbs(path string) bool { + return strings.HasPrefix(path, "/") || strings.HasPrefix(path, "#") +} + +// volumeNameLen returns length of the leading volume name on Windows. +// It returns 0 elsewhere. +func volumeNameLen(path string) int { + return 0 +} + +// HasPrefix exists for historical compatibility and should not be used. +// +// Deprecated: HasPrefix does not respect path boundaries and +// does not ignore case when required. +func HasPrefix(p, prefix string) bool { + return strings.HasPrefix(p, prefix) +} + +func splitList(path string) []string { + if path == "" { + return []string{} + } + return strings.Split(path, string(ListSeparator)) +} + +func abs(path string) (string, error) { + return unixAbs(path) +} + +func join(elem []string) string { + // If there's a bug here, fix the logic in ./path_unix.go too. + for i, e := range elem { + if e != "" { + return Clean(strings.Join(elem[i:], string(Separator))) + } + } + return "" +} + +func sameWord(a, b string) bool { + return a == b +} diff --git a/src/path/filepath/path_test.go b/src/path/filepath/path_test.go new file mode 100644 index 0000000..bd1904c --- /dev/null +++ b/src/path/filepath/path_test.go @@ -0,0 +1,1925 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filepath_test + +import ( + "errors" + "fmt" + "internal/testenv" + "io/fs" + "os" + "path/filepath" + "reflect" + "runtime" + "slices" + "sort" + "strings" + "syscall" + "testing" +) + +type PathTest struct { + path, result string +} + +var cleantests = []PathTest{ + // Already clean + {"abc", "abc"}, + {"abc/def", "abc/def"}, + {"a/b/c", "a/b/c"}, + {".", "."}, + {"..", ".."}, + {"../..", "../.."}, + {"../../abc", "../../abc"}, + {"/abc", "/abc"}, + {"/", "/"}, + + // Empty is current dir + {"", "."}, + + // Remove trailing slash + {"abc/", "abc"}, + {"abc/def/", "abc/def"}, + {"a/b/c/", "a/b/c"}, + {"./", "."}, + {"../", ".."}, + {"../../", "../.."}, + {"/abc/", "/abc"}, + + // Remove doubled slash + {"abc//def//ghi", "abc/def/ghi"}, + {"abc//", "abc"}, + + // Remove . elements + {"abc/./def", "abc/def"}, + {"/./abc/def", "/abc/def"}, + {"abc/.", "abc"}, + + // Remove .. elements + {"abc/def/ghi/../jkl", "abc/def/jkl"}, + {"abc/def/../ghi/../jkl", "abc/jkl"}, + {"abc/def/..", "abc"}, + {"abc/def/../..", "."}, + {"/abc/def/../..", "/"}, + {"abc/def/../../..", ".."}, + {"/abc/def/../../..", "/"}, + {"abc/def/../../../ghi/jkl/../../../mno", "../../mno"}, + {"/../abc", "/abc"}, + {"a/../b:/../../c", `../c`}, + + // Combinations + {"abc/./../def", "def"}, + {"abc//./../def", "def"}, + {"abc/../../././../def", "../../def"}, +} + +var nonwincleantests = []PathTest{ + // Remove leading doubled slash + {"//abc", "/abc"}, + {"///abc", "/abc"}, + {"//abc//", "/abc"}, +} + +var wincleantests = []PathTest{ + {`c:`, `c:.`}, + {`c:\`, `c:\`}, + {`c:\abc`, `c:\abc`}, + {`c:abc\..\..\.\.\..\def`, `c:..\..\def`}, + {`c:\abc\def\..\..`, `c:\`}, + {`c:\..\abc`, `c:\abc`}, + {`c:..\abc`, `c:..\abc`}, + {`c:\b:\..\..\..\d`, `c:\d`}, + {`\`, `\`}, + {`/`, `\`}, + {`\\i\..\c$`, `\\i\..\c$`}, + {`\\i\..\i\c$`, `\\i\..\i\c$`}, + {`\\i\..\I\c$`, `\\i\..\I\c$`}, + {`\\host\share\foo\..\bar`, `\\host\share\bar`}, + {`//host/share/foo/../baz`, `\\host\share\baz`}, + {`\\host\share\foo\..\..\..\..\bar`, `\\host\share\bar`}, + {`\\.\C:\a\..\..\..\..\bar`, `\\.\C:\bar`}, + {`\\.\C:\\\\a`, `\\.\C:\a`}, + {`\\a\b\..\c`, `\\a\b\c`}, + {`\\a\b`, `\\a\b`}, + {`.\c:`, `.\c:`}, + {`.\c:\foo`, `.\c:\foo`}, + {`.\c:foo`, `.\c:foo`}, + {`//abc`, `\\abc`}, + {`///abc`, `\\\abc`}, + {`//abc//`, `\\abc\\`}, + {`\\?\C:\`, `\\?\C:\`}, + {`\\?\C:\a`, `\\?\C:\a`}, + + // Don't allow cleaning to move an element with a colon to the start of the path. + {`a/../c:`, `.\c:`}, + {`a\..\c:`, `.\c:`}, + {`a/../c:/a`, `.\c:\a`}, + {`a/../../c:`, `..\c:`}, + {`foo:bar`, `foo:bar`}, + + // Don't allow cleaning to create a Root Local Device path like \??\a. + {`/a/../??/a`, `\.\??\a`}, +} + +func TestClean(t *testing.T) { + tests := cleantests + if runtime.GOOS == "windows" { + for i := range tests { + tests[i].result = filepath.FromSlash(tests[i].result) + } + tests = append(tests, wincleantests...) + } else { + tests = append(tests, nonwincleantests...) + } + for _, test := range tests { + if s := filepath.Clean(test.path); s != test.result { + t.Errorf("Clean(%q) = %q, want %q", test.path, s, test.result) + } + if s := filepath.Clean(test.result); s != test.result { + t.Errorf("Clean(%q) = %q, want %q", test.result, s, test.result) + } + } + + if testing.Short() { + t.Skip("skipping malloc count in short mode") + } + if runtime.GOMAXPROCS(0) > 1 { + t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1") + return + } + + for _, test := range tests { + allocs := testing.AllocsPerRun(100, func() { filepath.Clean(test.result) }) + if allocs > 0 { + t.Errorf("Clean(%q): %v allocs, want zero", test.result, allocs) + } + } +} + +type IsLocalTest struct { + path string + isLocal bool +} + +var islocaltests = []IsLocalTest{ + {"", false}, + {".", true}, + {"..", false}, + {"../a", false}, + {"/", false}, + {"/a", false}, + {"/a/../..", false}, + {"a", true}, + {"a/../a", true}, + {"a/", true}, + {"a/.", true}, + {"a/./b/./c", true}, + {`a/../b:/../../c`, false}, +} + +var winislocaltests = []IsLocalTest{ + {"NUL", false}, + {"nul", false}, + {"nul ", false}, + {"nul.", false}, + {"a/nul:", false}, + {"a/nul : a", false}, + {"com0", true}, + {"com1", false}, + {"com2", false}, + {"com3", false}, + {"com4", false}, + {"com5", false}, + {"com6", false}, + {"com7", false}, + {"com8", false}, + {"com9", false}, + {"com¹", false}, + {"com²", false}, + {"com³", false}, + {"com¹ : a", false}, + {"cOm1", false}, + {"lpt1", false}, + {"LPT1", false}, + {"lpt³", false}, + {"./nul", false}, + {`\`, false}, + {`\a`, false}, + {`C:`, false}, + {`C:\a`, false}, + {`..\a`, false}, + {`a/../c:`, false}, + {`CONIN$`, false}, + {`conin$`, false}, + {`CONOUT$`, false}, + {`conout$`, false}, + {`dollar$`, true}, // not a special file name +} + +var plan9islocaltests = []IsLocalTest{ + {"#a", false}, +} + +func TestIsLocal(t *testing.T) { + tests := islocaltests + if runtime.GOOS == "windows" { + tests = append(tests, winislocaltests...) + } + if runtime.GOOS == "plan9" { + tests = append(tests, plan9islocaltests...) + } + for _, test := range tests { + if got := filepath.IsLocal(test.path); got != test.isLocal { + t.Errorf("IsLocal(%q) = %v, want %v", test.path, got, test.isLocal) + } + } +} + +const sep = filepath.Separator + +var slashtests = []PathTest{ + {"", ""}, + {"/", string(sep)}, + {"/a/b", string([]byte{sep, 'a', sep, 'b'})}, + {"a//b", string([]byte{'a', sep, sep, 'b'})}, +} + +func TestFromAndToSlash(t *testing.T) { + for _, test := range slashtests { + if s := filepath.FromSlash(test.path); s != test.result { + t.Errorf("FromSlash(%q) = %q, want %q", test.path, s, test.result) + } + if s := filepath.ToSlash(test.result); s != test.path { + t.Errorf("ToSlash(%q) = %q, want %q", test.result, s, test.path) + } + } +} + +type SplitListTest struct { + list string + result []string +} + +const lsep = filepath.ListSeparator + +var splitlisttests = []SplitListTest{ + {"", []string{}}, + {string([]byte{'a', lsep, 'b'}), []string{"a", "b"}}, + {string([]byte{lsep, 'a', lsep, 'b'}), []string{"", "a", "b"}}, +} + +var winsplitlisttests = []SplitListTest{ + // quoted + {`"a"`, []string{`a`}}, + + // semicolon + {`";"`, []string{`;`}}, + {`"a;b"`, []string{`a;b`}}, + {`";";`, []string{`;`, ``}}, + {`;";"`, []string{``, `;`}}, + + // partially quoted + {`a";"b`, []string{`a;b`}}, + {`a; ""b`, []string{`a`, ` b`}}, + {`"a;b`, []string{`a;b`}}, + {`""a;b`, []string{`a`, `b`}}, + {`"""a;b`, []string{`a;b`}}, + {`""""a;b`, []string{`a`, `b`}}, + {`a";b`, []string{`a;b`}}, + {`a;b";c`, []string{`a`, `b;c`}}, + {`"a";b";c`, []string{`a`, `b;c`}}, +} + +func TestSplitList(t *testing.T) { + tests := splitlisttests + if runtime.GOOS == "windows" { + tests = append(tests, winsplitlisttests...) + } + for _, test := range tests { + if l := filepath.SplitList(test.list); !reflect.DeepEqual(l, test.result) { + t.Errorf("SplitList(%#q) = %#q, want %#q", test.list, l, test.result) + } + } +} + +type SplitTest struct { + path, dir, file string +} + +var unixsplittests = []SplitTest{ + {"a/b", "a/", "b"}, + {"a/b/", "a/b/", ""}, + {"a/", "a/", ""}, + {"a", "", "a"}, + {"/", "/", ""}, +} + +var winsplittests = []SplitTest{ + {`c:`, `c:`, ``}, + {`c:/`, `c:/`, ``}, + {`c:/foo`, `c:/`, `foo`}, + {`c:/foo/bar`, `c:/foo/`, `bar`}, + {`//host/share`, `//host/share`, ``}, + {`//host/share/`, `//host/share/`, ``}, + {`//host/share/foo`, `//host/share/`, `foo`}, + {`\\host\share`, `\\host\share`, ``}, + {`\\host\share\`, `\\host\share\`, ``}, + {`\\host\share\foo`, `\\host\share\`, `foo`}, +} + +func TestSplit(t *testing.T) { + var splittests []SplitTest + splittests = unixsplittests + if runtime.GOOS == "windows" { + splittests = append(splittests, winsplittests...) + } + for _, test := range splittests { + if d, f := filepath.Split(test.path); d != test.dir || f != test.file { + t.Errorf("Split(%q) = %q, %q, want %q, %q", test.path, d, f, test.dir, test.file) + } + } +} + +type JoinTest struct { + elem []string + path string +} + +var jointests = []JoinTest{ + // zero parameters + {[]string{}, ""}, + + // one parameter + {[]string{""}, ""}, + {[]string{"/"}, "/"}, + {[]string{"a"}, "a"}, + + // two parameters + {[]string{"a", "b"}, "a/b"}, + {[]string{"a", ""}, "a"}, + {[]string{"", "b"}, "b"}, + {[]string{"/", "a"}, "/a"}, + {[]string{"/", "a/b"}, "/a/b"}, + {[]string{"/", ""}, "/"}, + {[]string{"/a", "b"}, "/a/b"}, + {[]string{"a", "/b"}, "a/b"}, + {[]string{"/a", "/b"}, "/a/b"}, + {[]string{"a/", "b"}, "a/b"}, + {[]string{"a/", ""}, "a"}, + {[]string{"", ""}, ""}, + + // three parameters + {[]string{"/", "a", "b"}, "/a/b"}, +} + +var nonwinjointests = []JoinTest{ + {[]string{"//", "a"}, "/a"}, +} + +var winjointests = []JoinTest{ + {[]string{`directory`, `file`}, `directory\file`}, + {[]string{`C:\Windows\`, `System32`}, `C:\Windows\System32`}, + {[]string{`C:\Windows\`, ``}, `C:\Windows`}, + {[]string{`C:\`, `Windows`}, `C:\Windows`}, + {[]string{`C:`, `a`}, `C:a`}, + {[]string{`C:`, `a\b`}, `C:a\b`}, + {[]string{`C:`, `a`, `b`}, `C:a\b`}, + {[]string{`C:`, ``, `b`}, `C:b`}, + {[]string{`C:`, ``, ``, `b`}, `C:b`}, + {[]string{`C:`, ``}, `C:.`}, + {[]string{`C:`, ``, ``}, `C:.`}, + {[]string{`C:`, `\a`}, `C:\a`}, + {[]string{`C:`, ``, `\a`}, `C:\a`}, + {[]string{`C:.`, `a`}, `C:a`}, + {[]string{`C:a`, `b`}, `C:a\b`}, + {[]string{`C:a`, `b`, `d`}, `C:a\b\d`}, + {[]string{`\\host\share`, `foo`}, `\\host\share\foo`}, + {[]string{`\\host\share\foo`}, `\\host\share\foo`}, + {[]string{`//host/share`, `foo/bar`}, `\\host\share\foo\bar`}, + {[]string{`\`}, `\`}, + {[]string{`\`, ``}, `\`}, + {[]string{`\`, `a`}, `\a`}, + {[]string{`\\`, `a`}, `\\a`}, + {[]string{`\`, `a`, `b`}, `\a\b`}, + {[]string{`\\`, `a`, `b`}, `\\a\b`}, + {[]string{`\`, `\\a\b`, `c`}, `\a\b\c`}, + {[]string{`\\a`, `b`, `c`}, `\\a\b\c`}, + {[]string{`\\a\`, `b`, `c`}, `\\a\b\c`}, + {[]string{`//`, `a`}, `\\a`}, + {[]string{`a:\b\c`, `x\..\y:\..\..\z`}, `a:\b\z`}, + {[]string{`\`, `??\a`}, `\.\??\a`}, +} + +func TestJoin(t *testing.T) { + if runtime.GOOS == "windows" { + jointests = append(jointests, winjointests...) + } else { + jointests = append(jointests, nonwinjointests...) + } + for _, test := range jointests { + expected := filepath.FromSlash(test.path) + if p := filepath.Join(test.elem...); p != expected { + t.Errorf("join(%q) = %q, want %q", test.elem, p, expected) + } + } +} + +type ExtTest struct { + path, ext string +} + +var exttests = []ExtTest{ + {"path.go", ".go"}, + {"path.pb.go", ".go"}, + {"a.dir/b", ""}, + {"a.dir/b.go", ".go"}, + {"a.dir/", ""}, +} + +func TestExt(t *testing.T) { + for _, test := range exttests { + if x := filepath.Ext(test.path); x != test.ext { + t.Errorf("Ext(%q) = %q, want %q", test.path, x, test.ext) + } + } +} + +type Node struct { + name string + entries []*Node // nil if the entry is a file + mark int +} + +var tree = &Node{ + "testdata", + []*Node{ + {"a", nil, 0}, + {"b", []*Node{}, 0}, + {"c", nil, 0}, + { + "d", + []*Node{ + {"x", nil, 0}, + {"y", []*Node{}, 0}, + { + "z", + []*Node{ + {"u", nil, 0}, + {"v", nil, 0}, + }, + 0, + }, + }, + 0, + }, + }, + 0, +} + +func walkTree(n *Node, path string, f func(path string, n *Node)) { + f(path, n) + for _, e := range n.entries { + walkTree(e, filepath.Join(path, e.name), f) + } +} + +func makeTree(t *testing.T) { + walkTree(tree, tree.name, func(path string, n *Node) { + if n.entries == nil { + fd, err := os.Create(path) + if err != nil { + t.Errorf("makeTree: %v", err) + return + } + fd.Close() + } else { + os.Mkdir(path, 0770) + } + }) +} + +func markTree(n *Node) { walkTree(n, "", func(path string, n *Node) { n.mark++ }) } + +func checkMarks(t *testing.T, report bool) { + walkTree(tree, tree.name, func(path string, n *Node) { + if n.mark != 1 && report { + t.Errorf("node %s mark = %d; expected 1", path, n.mark) + } + n.mark = 0 + }) +} + +// Assumes that each node name is unique. Good enough for a test. +// If clear is true, any incoming error is cleared before return. The errors +// are always accumulated, though. +func mark(d fs.DirEntry, err error, errors *[]error, clear bool) error { + name := d.Name() + walkTree(tree, tree.name, func(path string, n *Node) { + if n.name == name { + n.mark++ + } + }) + if err != nil { + *errors = append(*errors, err) + if clear { + return nil + } + return err + } + return nil +} + +// chdir changes the current working directory to the named directory, +// and then restore the original working directory at the end of the test. +func chdir(t *testing.T, dir string) { + olddir, err := os.Getwd() + if err != nil { + t.Fatalf("getwd %s: %v", dir, err) + } + if err := os.Chdir(dir); err != nil { + t.Fatalf("chdir %s: %v", dir, err) + } + + t.Cleanup(func() { + if err := os.Chdir(olddir); err != nil { + t.Errorf("restore original working directory %s: %v", olddir, err) + os.Exit(1) + } + }) +} + +func chtmpdir(t *testing.T) (restore func()) { + oldwd, err := os.Getwd() + if err != nil { + t.Fatalf("chtmpdir: %v", err) + } + d, err := os.MkdirTemp("", "test") + if err != nil { + t.Fatalf("chtmpdir: %v", err) + } + if err := os.Chdir(d); err != nil { + t.Fatalf("chtmpdir: %v", err) + } + return func() { + if err := os.Chdir(oldwd); err != nil { + t.Fatalf("chtmpdir: %v", err) + } + os.RemoveAll(d) + } +} + +// tempDirCanonical returns a temporary directory for the test to use, ensuring +// that the returned path does not contain symlinks. +func tempDirCanonical(t *testing.T) string { + dir := t.TempDir() + + cdir, err := filepath.EvalSymlinks(dir) + if err != nil { + t.Errorf("tempDirCanonical: %v", err) + } + + return cdir +} + +func TestWalk(t *testing.T) { + walk := func(root string, fn fs.WalkDirFunc) error { + return filepath.Walk(root, func(path string, info fs.FileInfo, err error) error { + return fn(path, &statDirEntry{info}, err) + }) + } + testWalk(t, walk, 1) +} + +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 } + +func (d *statDirEntry) String() string { + return fs.FormatDirEntry(d) +} + +func TestWalkDir(t *testing.T) { + testWalk(t, filepath.WalkDir, 2) +} + +func testWalk(t *testing.T, walk func(string, fs.WalkDirFunc) error, errVisit int) { + if runtime.GOOS == "ios" { + restore := chtmpdir(t) + defer restore() + } + + tmpDir := t.TempDir() + + origDir, err := os.Getwd() + if err != nil { + t.Fatal("finding working dir:", err) + } + if err = os.Chdir(tmpDir); err != nil { + t.Fatal("entering temp dir:", err) + } + defer os.Chdir(origDir) + + makeTree(t) + errors := make([]error, 0, 10) + clear := true + markFn := func(path string, d fs.DirEntry, err error) error { + return mark(d, err, &errors, clear) + } + // Expect no errors. + err = walk(tree.name, markFn) + if err != nil { + t.Fatalf("no error expected, found: %s", err) + } + if len(errors) != 0 { + t.Fatalf("unexpected errors: %s", errors) + } + checkMarks(t, true) + errors = errors[0:0] + + t.Run("PermErr", func(t *testing.T) { + // Test permission errors. Only possible if we're not root + // and only on some file systems (AFS, FAT). To avoid errors during + // all.bash on those file systems, skip during go test -short. + // Chmod is not supported on wasip1. + if runtime.GOOS == "windows" || runtime.GOOS == "wasip1" { + t.Skip("skipping on " + runtime.GOOS) + } + if os.Getuid() == 0 { + t.Skip("skipping as root") + } + if testing.Short() { + t.Skip("skipping in short mode") + } + + // introduce 2 errors: chmod top-level directories to 0 + os.Chmod(filepath.Join(tree.name, tree.entries[1].name), 0) + os.Chmod(filepath.Join(tree.name, tree.entries[3].name), 0) + + // 3) capture errors, expect two. + // mark respective subtrees manually + markTree(tree.entries[1]) + markTree(tree.entries[3]) + // correct double-marking of directory itself + tree.entries[1].mark -= errVisit + tree.entries[3].mark -= errVisit + err := walk(tree.name, markFn) + if err != nil { + t.Fatalf("expected no error return from Walk, got %s", err) + } + if len(errors) != 2 { + t.Errorf("expected 2 errors, got %d: %s", len(errors), errors) + } + // the inaccessible subtrees were marked manually + checkMarks(t, true) + errors = errors[0:0] + + // 4) capture errors, stop after first error. + // mark respective subtrees manually + markTree(tree.entries[1]) + markTree(tree.entries[3]) + // correct double-marking of directory itself + tree.entries[1].mark -= errVisit + tree.entries[3].mark -= errVisit + clear = false // error will stop processing + err = walk(tree.name, markFn) + if err == nil { + t.Fatalf("expected error return from Walk") + } + if len(errors) != 1 { + t.Errorf("expected 1 error, got %d: %s", len(errors), errors) + } + // the inaccessible subtrees were marked manually + checkMarks(t, false) + errors = errors[0:0] + + // restore permissions + os.Chmod(filepath.Join(tree.name, tree.entries[1].name), 0770) + os.Chmod(filepath.Join(tree.name, tree.entries[3].name), 0770) + }) +} + +func touch(t *testing.T, name string) { + f, err := os.Create(name) + if err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } +} + +func TestWalkSkipDirOnFile(t *testing.T) { + td := t.TempDir() + + if err := os.MkdirAll(filepath.Join(td, "dir"), 0755); err != nil { + t.Fatal(err) + } + touch(t, filepath.Join(td, "dir/foo1")) + touch(t, filepath.Join(td, "dir/foo2")) + + sawFoo2 := false + walker := func(path string) error { + if strings.HasSuffix(path, "foo2") { + sawFoo2 = true + } + if strings.HasSuffix(path, "foo1") { + return filepath.SkipDir + } + return nil + } + walkFn := func(path string, _ fs.FileInfo, _ error) error { return walker(path) } + walkDirFn := func(path string, _ fs.DirEntry, _ error) error { return walker(path) } + + check := func(t *testing.T, walk func(root string) error, root string) { + t.Helper() + sawFoo2 = false + err := walk(root) + if err != nil { + t.Fatal(err) + } + if sawFoo2 { + t.Errorf("SkipDir on file foo1 did not block processing of foo2") + } + } + + t.Run("Walk", func(t *testing.T) { + Walk := func(root string) error { return filepath.Walk(td, walkFn) } + check(t, Walk, td) + check(t, Walk, filepath.Join(td, "dir")) + }) + t.Run("WalkDir", func(t *testing.T) { + WalkDir := func(root string) error { return filepath.WalkDir(td, walkDirFn) } + check(t, WalkDir, td) + check(t, WalkDir, filepath.Join(td, "dir")) + }) +} + +func TestWalkSkipAllOnFile(t *testing.T) { + td := t.TempDir() + + if err := os.MkdirAll(filepath.Join(td, "dir", "subdir"), 0755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(td, "dir2"), 0755); err != nil { + t.Fatal(err) + } + + touch(t, filepath.Join(td, "dir", "foo1")) + touch(t, filepath.Join(td, "dir", "foo2")) + touch(t, filepath.Join(td, "dir", "subdir", "foo3")) + touch(t, filepath.Join(td, "dir", "foo4")) + touch(t, filepath.Join(td, "dir2", "bar")) + touch(t, filepath.Join(td, "last")) + + remainingWereSkipped := true + walker := func(path string) error { + if strings.HasSuffix(path, "foo2") { + return filepath.SkipAll + } + + if strings.HasSuffix(path, "foo3") || + strings.HasSuffix(path, "foo4") || + strings.HasSuffix(path, "bar") || + strings.HasSuffix(path, "last") { + remainingWereSkipped = false + } + return nil + } + + walkFn := func(path string, _ fs.FileInfo, _ error) error { return walker(path) } + walkDirFn := func(path string, _ fs.DirEntry, _ error) error { return walker(path) } + + check := func(t *testing.T, walk func(root string) error, root string) { + t.Helper() + remainingWereSkipped = true + if err := walk(root); err != nil { + t.Fatal(err) + } + if !remainingWereSkipped { + t.Errorf("SkipAll on file foo2 did not block processing of remaining files and directories") + } + } + + t.Run("Walk", func(t *testing.T) { + Walk := func(_ string) error { return filepath.Walk(td, walkFn) } + check(t, Walk, td) + check(t, Walk, filepath.Join(td, "dir")) + }) + t.Run("WalkDir", func(t *testing.T) { + WalkDir := func(_ string) error { return filepath.WalkDir(td, walkDirFn) } + check(t, WalkDir, td) + check(t, WalkDir, filepath.Join(td, "dir")) + }) +} + +func TestWalkFileError(t *testing.T) { + td := t.TempDir() + + touch(t, filepath.Join(td, "foo")) + touch(t, filepath.Join(td, "bar")) + dir := filepath.Join(td, "dir") + if err := os.MkdirAll(filepath.Join(td, "dir"), 0755); err != nil { + t.Fatal(err) + } + touch(t, filepath.Join(dir, "baz")) + touch(t, filepath.Join(dir, "stat-error")) + defer func() { + *filepath.LstatP = os.Lstat + }() + statErr := errors.New("some stat error") + *filepath.LstatP = func(path string) (fs.FileInfo, error) { + if strings.HasSuffix(path, "stat-error") { + return nil, statErr + } + return os.Lstat(path) + } + got := map[string]error{} + err := filepath.Walk(td, func(path string, fi fs.FileInfo, err error) error { + rel, _ := filepath.Rel(td, path) + got[filepath.ToSlash(rel)] = err + return nil + }) + if err != nil { + t.Errorf("Walk error: %v", err) + } + want := map[string]error{ + ".": nil, + "foo": nil, + "bar": nil, + "dir": nil, + "dir/baz": nil, + "dir/stat-error": statErr, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Walked %#v; want %#v", got, want) + } +} + +func TestWalkSymlinkRoot(t *testing.T) { + testenv.MustHaveSymlink(t) + + td := t.TempDir() + dir := filepath.Join(td, "dir") + if err := os.MkdirAll(filepath.Join(td, "dir"), 0755); err != nil { + t.Fatal(err) + } + touch(t, filepath.Join(dir, "foo")) + + link := filepath.Join(td, "link") + if err := os.Symlink("dir", link); err != nil { + t.Fatal(err) + } + + abslink := filepath.Join(td, "abslink") + if err := os.Symlink(dir, abslink); err != nil { + t.Fatal(err) + } + + linklink := filepath.Join(td, "linklink") + if err := os.Symlink("link", linklink); err != nil { + t.Fatal(err) + } + + // Per https://pubs.opengroup.org/onlinepubs/9699919799.2013edition/basedefs/V1_chap04.html#tag_04_12: + // “A pathname that contains at least one non- <slash> character and that ends + // with one or more trailing <slash> characters shall not be resolved + // successfully unless the last pathname component before the trailing <slash> + // characters names an existing directory [...].” + // + // Since Walk does not traverse symlinks itself, its behavior should depend on + // whether the path passed to Walk ends in a slash: if it does not end in a slash, + // Walk should report the symlink itself (since it is the last pathname component); + // but if it does end in a slash, Walk should walk the directory to which the symlink + // refers (since it must be fully resolved before walking). + for _, tt := range []struct { + desc string + root string + want []string + buggyGOOS []string + }{ + { + desc: "no slash", + root: link, + want: []string{link}, + }, + { + desc: "slash", + root: link + string(filepath.Separator), + want: []string{link, filepath.Join(link, "foo")}, + }, + { + desc: "abs no slash", + root: abslink, + want: []string{abslink}, + }, + { + desc: "abs with slash", + root: abslink + string(filepath.Separator), + want: []string{abslink, filepath.Join(abslink, "foo")}, + }, + { + desc: "double link no slash", + root: linklink, + want: []string{linklink}, + }, + { + desc: "double link with slash", + root: linklink + string(filepath.Separator), + want: []string{linklink, filepath.Join(linklink, "foo")}, + buggyGOOS: []string{"darwin", "ios"}, // https://go.dev/issue/59586 + }, + } { + tt := tt + t.Run(tt.desc, func(t *testing.T) { + var walked []string + err := filepath.Walk(tt.root, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + t.Logf("%#q: %v", path, info.Mode()) + walked = append(walked, filepath.Clean(path)) + return nil + }) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(walked, tt.want) { + t.Logf("Walk(%#q) visited %#q; want %#q", tt.root, walked, tt.want) + if slices.Contains(tt.buggyGOOS, runtime.GOOS) { + t.Logf("(ignoring known bug on %v)", runtime.GOOS) + } else { + t.Fail() + } + } + }) + } +} + +var basetests = []PathTest{ + {"", "."}, + {".", "."}, + {"/.", "."}, + {"/", "/"}, + {"////", "/"}, + {"x/", "x"}, + {"abc", "abc"}, + {"abc/def", "def"}, + {"a/b/.x", ".x"}, + {"a/b/c.", "c."}, + {"a/b/c.x", "c.x"}, +} + +var winbasetests = []PathTest{ + {`c:\`, `\`}, + {`c:.`, `.`}, + {`c:\a\b`, `b`}, + {`c:a\b`, `b`}, + {`c:a\b\c`, `c`}, + {`\\host\share\`, `\`}, + {`\\host\share\a`, `a`}, + {`\\host\share\a\b`, `b`}, +} + +func TestBase(t *testing.T) { + tests := basetests + if runtime.GOOS == "windows" { + // make unix tests work on windows + for i := range tests { + tests[i].result = filepath.Clean(tests[i].result) + } + // add windows specific tests + tests = append(tests, winbasetests...) + } + for _, test := range tests { + if s := filepath.Base(test.path); s != test.result { + t.Errorf("Base(%q) = %q, want %q", test.path, s, test.result) + } + } +} + +var dirtests = []PathTest{ + {"", "."}, + {".", "."}, + {"/.", "/"}, + {"/", "/"}, + {"/foo", "/"}, + {"x/", "x"}, + {"abc", "."}, + {"abc/def", "abc"}, + {"a/b/.x", "a/b"}, + {"a/b/c.", "a/b"}, + {"a/b/c.x", "a/b"}, +} + +var nonwindirtests = []PathTest{ + {"////", "/"}, +} + +var windirtests = []PathTest{ + {`c:\`, `c:\`}, + {`c:.`, `c:.`}, + {`c:\a\b`, `c:\a`}, + {`c:a\b`, `c:a`}, + {`c:a\b\c`, `c:a\b`}, + {`\\host\share`, `\\host\share`}, + {`\\host\share\`, `\\host\share\`}, + {`\\host\share\a`, `\\host\share\`}, + {`\\host\share\a\b`, `\\host\share\a`}, + {`\\\\`, `\\\\`}, +} + +func TestDir(t *testing.T) { + tests := dirtests + if runtime.GOOS == "windows" { + // make unix tests work on windows + for i := range tests { + tests[i].result = filepath.Clean(tests[i].result) + } + // add windows specific tests + tests = append(tests, windirtests...) + } else { + tests = append(tests, nonwindirtests...) + } + for _, test := range tests { + if s := filepath.Dir(test.path); s != test.result { + t.Errorf("Dir(%q) = %q, want %q", test.path, s, test.result) + } + } +} + +type IsAbsTest struct { + path string + isAbs bool +} + +var isabstests = []IsAbsTest{ + {"", false}, + {"/", true}, + {"/usr/bin/gcc", true}, + {"..", false}, + {"/a/../bb", true}, + {".", false}, + {"./", false}, + {"lala", false}, +} + +var winisabstests = []IsAbsTest{ + {`C:\`, true}, + {`c\`, false}, + {`c::`, false}, + {`c:`, false}, + {`/`, false}, + {`\`, false}, + {`\Windows`, false}, + {`c:a\b`, false}, + {`c:\a\b`, true}, + {`c:/a/b`, true}, + {`\\host\share`, true}, + {`\\host\share\`, true}, + {`\\host\share\foo`, true}, + {`//host/share/foo/bar`, true}, + {`\\?\a\b\c`, true}, + {`\??\a\b\c`, true}, +} + +func TestIsAbs(t *testing.T) { + var tests []IsAbsTest + if runtime.GOOS == "windows" { + tests = append(tests, winisabstests...) + // All non-windows tests should fail, because they have no volume letter. + for _, test := range isabstests { + tests = append(tests, IsAbsTest{test.path, false}) + } + // All non-windows test should work as intended if prefixed with volume letter. + for _, test := range isabstests { + tests = append(tests, IsAbsTest{"c:" + test.path, test.isAbs}) + } + } else { + tests = isabstests + } + + for _, test := range tests { + if r := filepath.IsAbs(test.path); r != test.isAbs { + t.Errorf("IsAbs(%q) = %v, want %v", test.path, r, test.isAbs) + } + } +} + +type EvalSymlinksTest struct { + // If dest is empty, the path is created; otherwise the dest is symlinked to the path. + path, dest string +} + +var EvalSymlinksTestDirs = []EvalSymlinksTest{ + {"test", ""}, + {"test/dir", ""}, + {"test/dir/link3", "../../"}, + {"test/link1", "../test"}, + {"test/link2", "dir"}, + {"test/linkabs", "/"}, + {"test/link4", "../test2"}, + {"test2", "test/dir"}, + // Issue 23444. + {"src", ""}, + {"src/pool", ""}, + {"src/pool/test", ""}, + {"src/versions", ""}, + {"src/versions/current", "../../version"}, + {"src/versions/v1", ""}, + {"src/versions/v1/modules", ""}, + {"src/versions/v1/modules/test", "../../../pool/test"}, + {"version", "src/versions/v1"}, +} + +var EvalSymlinksTests = []EvalSymlinksTest{ + {"test", "test"}, + {"test/dir", "test/dir"}, + {"test/dir/../..", "."}, + {"test/link1", "test"}, + {"test/link2", "test/dir"}, + {"test/link1/dir", "test/dir"}, + {"test/link2/..", "test"}, + {"test/dir/link3", "."}, + {"test/link2/link3/test", "test"}, + {"test/linkabs", "/"}, + {"test/link4/..", "test"}, + {"src/versions/current/modules/test", "src/pool/test"}, +} + +// simpleJoin builds a file name from the directory and path. +// It does not use Join because we don't want ".." to be evaluated. +func simpleJoin(dir, path string) string { + return dir + string(filepath.Separator) + path +} + +func testEvalSymlinks(t *testing.T, path, want string) { + have, err := filepath.EvalSymlinks(path) + if err != nil { + t.Errorf("EvalSymlinks(%q) error: %v", path, err) + return + } + if filepath.Clean(have) != filepath.Clean(want) { + t.Errorf("EvalSymlinks(%q) returns %q, want %q", path, have, want) + } +} + +func testEvalSymlinksAfterChdir(t *testing.T, wd, path, want string) { + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + err := os.Chdir(cwd) + if err != nil { + t.Fatal(err) + } + }() + + err = os.Chdir(wd) + if err != nil { + t.Fatal(err) + } + + have, err := filepath.EvalSymlinks(path) + if err != nil { + t.Errorf("EvalSymlinks(%q) in %q directory error: %v", path, wd, err) + return + } + if filepath.Clean(have) != filepath.Clean(want) { + t.Errorf("EvalSymlinks(%q) in %q directory returns %q, want %q", path, wd, have, want) + } +} + +func TestEvalSymlinks(t *testing.T) { + testenv.MustHaveSymlink(t) + + tmpDir := t.TempDir() + + // /tmp may itself be a symlink! Avoid the confusion, although + // it means trusting the thing we're testing. + var err error + tmpDir, err = filepath.EvalSymlinks(tmpDir) + if err != nil { + t.Fatal("eval symlink for tmp dir:", err) + } + + // Create the symlink farm using relative paths. + for _, d := range EvalSymlinksTestDirs { + var err error + path := simpleJoin(tmpDir, d.path) + if d.dest == "" { + err = os.Mkdir(path, 0755) + } else { + err = os.Symlink(d.dest, path) + } + if err != nil { + t.Fatal(err) + } + } + + // Evaluate the symlink farm. + for _, test := range EvalSymlinksTests { + path := simpleJoin(tmpDir, test.path) + + dest := simpleJoin(tmpDir, test.dest) + if filepath.IsAbs(test.dest) || os.IsPathSeparator(test.dest[0]) { + dest = test.dest + } + testEvalSymlinks(t, path, dest) + + // test EvalSymlinks(".") + testEvalSymlinksAfterChdir(t, path, ".", ".") + + // test EvalSymlinks("C:.") on Windows + if runtime.GOOS == "windows" { + volDot := filepath.VolumeName(tmpDir) + "." + testEvalSymlinksAfterChdir(t, path, volDot, volDot) + } + + // test EvalSymlinks(".."+path) + dotdotPath := simpleJoin("..", test.dest) + if filepath.IsAbs(test.dest) || os.IsPathSeparator(test.dest[0]) { + dotdotPath = test.dest + } + testEvalSymlinksAfterChdir(t, + simpleJoin(tmpDir, "test"), + simpleJoin("..", test.path), + dotdotPath) + + // test EvalSymlinks(p) where p is relative path + testEvalSymlinksAfterChdir(t, tmpDir, test.path, test.dest) + } +} + +func TestEvalSymlinksIsNotExist(t *testing.T) { + testenv.MustHaveSymlink(t) + + defer chtmpdir(t)() + + _, err := filepath.EvalSymlinks("notexist") + if !os.IsNotExist(err) { + t.Errorf("expected the file is not found, got %v\n", err) + } + + err = os.Symlink("notexist", "link") + if err != nil { + t.Fatal(err) + } + defer os.Remove("link") + + _, err = filepath.EvalSymlinks("link") + if !os.IsNotExist(err) { + t.Errorf("expected the file is not found, got %v\n", err) + } +} + +func TestIssue13582(t *testing.T) { + testenv.MustHaveSymlink(t) + + tmpDir := t.TempDir() + + dir := filepath.Join(tmpDir, "dir") + err := os.Mkdir(dir, 0755) + if err != nil { + t.Fatal(err) + } + linkToDir := filepath.Join(tmpDir, "link_to_dir") + err = os.Symlink(dir, linkToDir) + if err != nil { + t.Fatal(err) + } + file := filepath.Join(linkToDir, "file") + err = os.WriteFile(file, nil, 0644) + if err != nil { + t.Fatal(err) + } + link1 := filepath.Join(linkToDir, "link1") + err = os.Symlink(file, link1) + if err != nil { + t.Fatal(err) + } + link2 := filepath.Join(linkToDir, "link2") + err = os.Symlink(link1, link2) + if err != nil { + t.Fatal(err) + } + + // /tmp may itself be a symlink! + realTmpDir, err := filepath.EvalSymlinks(tmpDir) + if err != nil { + t.Fatal(err) + } + realDir := filepath.Join(realTmpDir, "dir") + realFile := filepath.Join(realDir, "file") + + tests := []struct { + path, want string + }{ + {dir, realDir}, + {linkToDir, realDir}, + {file, realFile}, + {link1, realFile}, + {link2, realFile}, + } + for i, test := range tests { + have, err := filepath.EvalSymlinks(test.path) + if err != nil { + t.Fatal(err) + } + if have != test.want { + t.Errorf("test#%d: EvalSymlinks(%q) returns %q, want %q", i, test.path, have, test.want) + } + } +} + +// Issue 57905. +func TestRelativeSymlinkToAbsolute(t *testing.T) { + testenv.MustHaveSymlink(t) + // Not parallel: uses os.Chdir. + + tmpDir := t.TempDir() + chdir(t, tmpDir) + + // Create "link" in the current working directory as a symlink to an arbitrary + // absolute path. On macOS, this path is likely to begin with a symlink + // itself: generally either in /var (symlinked to "private/var") or /tmp + // (symlinked to "private/tmp"). + if err := os.Symlink(tmpDir, "link"); err != nil { + t.Fatal(err) + } + t.Logf(`os.Symlink(%q, "link")`, tmpDir) + + p, err := filepath.EvalSymlinks("link") + if err != nil { + t.Fatalf(`EvalSymlinks("link"): %v`, err) + } + want, err := filepath.EvalSymlinks(tmpDir) + if err != nil { + t.Fatalf(`EvalSymlinks(%q): %v`, tmpDir, err) + } + if p != want { + t.Errorf(`EvalSymlinks("link") = %q; want %q`, p, want) + } + t.Logf(`EvalSymlinks("link") = %q`, p) +} + +// Test directories relative to temporary directory. +// The tests are run in absTestDirs[0]. +var absTestDirs = []string{ + "a", + "a/b", + "a/b/c", +} + +// Test paths relative to temporary directory. $ expands to the directory. +// The tests are run in absTestDirs[0]. +// We create absTestDirs first. +var absTests = []string{ + ".", + "b", + "b/", + "../a", + "../a/b", + "../a/b/./c/../../.././a", + "../a/b/./c/../../.././a/", + "$", + "$/.", + "$/a/../a/b", + "$/a/b/c/../../.././a", + "$/a/b/c/../../.././a/", +} + +func TestAbs(t *testing.T) { + root := t.TempDir() + wd, err := os.Getwd() + if err != nil { + t.Fatal("getwd failed: ", err) + } + err = os.Chdir(root) + if err != nil { + t.Fatal("chdir failed: ", err) + } + defer os.Chdir(wd) + + for _, dir := range absTestDirs { + err = os.Mkdir(dir, 0777) + if err != nil { + t.Fatal("Mkdir failed: ", err) + } + } + + if runtime.GOOS == "windows" { + vol := filepath.VolumeName(root) + var extra []string + for _, path := range absTests { + if strings.Contains(path, "$") { + continue + } + path = vol + path + extra = append(extra, path) + } + absTests = append(absTests, extra...) + } + + err = os.Chdir(absTestDirs[0]) + if err != nil { + t.Fatal("chdir failed: ", err) + } + + for _, path := range absTests { + path = strings.ReplaceAll(path, "$", root) + info, err := os.Stat(path) + if err != nil { + t.Errorf("%s: %s", path, err) + continue + } + + abspath, err := filepath.Abs(path) + if err != nil { + t.Errorf("Abs(%q) error: %v", path, err) + continue + } + absinfo, err := os.Stat(abspath) + if err != nil || !os.SameFile(absinfo, info) { + t.Errorf("Abs(%q)=%q, not the same file", path, abspath) + } + if !filepath.IsAbs(abspath) { + t.Errorf("Abs(%q)=%q, not an absolute path", path, abspath) + } + if filepath.IsAbs(abspath) && abspath != filepath.Clean(abspath) { + t.Errorf("Abs(%q)=%q, isn't clean", path, abspath) + } + } +} + +// Empty path needs to be special-cased on Windows. See golang.org/issue/24441. +// We test it separately from all other absTests because the empty string is not +// a valid path, so it can't be used with os.Stat. +func TestAbsEmptyString(t *testing.T) { + root := t.TempDir() + + wd, err := os.Getwd() + if err != nil { + t.Fatal("getwd failed: ", err) + } + err = os.Chdir(root) + if err != nil { + t.Fatal("chdir failed: ", err) + } + defer os.Chdir(wd) + + info, err := os.Stat(root) + if err != nil { + t.Fatalf("%s: %s", root, err) + } + + abspath, err := filepath.Abs("") + if err != nil { + t.Fatalf(`Abs("") error: %v`, err) + } + absinfo, err := os.Stat(abspath) + if err != nil || !os.SameFile(absinfo, info) { + t.Errorf(`Abs("")=%q, not the same file`, abspath) + } + if !filepath.IsAbs(abspath) { + t.Errorf(`Abs("")=%q, not an absolute path`, abspath) + } + if filepath.IsAbs(abspath) && abspath != filepath.Clean(abspath) { + t.Errorf(`Abs("")=%q, isn't clean`, abspath) + } +} + +type RelTests struct { + root, path, want string +} + +var reltests = []RelTests{ + {"a/b", "a/b", "."}, + {"a/b/.", "a/b", "."}, + {"a/b", "a/b/.", "."}, + {"./a/b", "a/b", "."}, + {"a/b", "./a/b", "."}, + {"ab/cd", "ab/cde", "../cde"}, + {"ab/cd", "ab/c", "../c"}, + {"a/b", "a/b/c/d", "c/d"}, + {"a/b", "a/b/../c", "../c"}, + {"a/b/../c", "a/b", "../b"}, + {"a/b/c", "a/c/d", "../../c/d"}, + {"a/b", "c/d", "../../c/d"}, + {"a/b/c/d", "a/b", "../.."}, + {"a/b/c/d", "a/b/", "../.."}, + {"a/b/c/d/", "a/b", "../.."}, + {"a/b/c/d/", "a/b/", "../.."}, + {"../../a/b", "../../a/b/c/d", "c/d"}, + {"/a/b", "/a/b", "."}, + {"/a/b/.", "/a/b", "."}, + {"/a/b", "/a/b/.", "."}, + {"/ab/cd", "/ab/cde", "../cde"}, + {"/ab/cd", "/ab/c", "../c"}, + {"/a/b", "/a/b/c/d", "c/d"}, + {"/a/b", "/a/b/../c", "../c"}, + {"/a/b/../c", "/a/b", "../b"}, + {"/a/b/c", "/a/c/d", "../../c/d"}, + {"/a/b", "/c/d", "../../c/d"}, + {"/a/b/c/d", "/a/b", "../.."}, + {"/a/b/c/d", "/a/b/", "../.."}, + {"/a/b/c/d/", "/a/b", "../.."}, + {"/a/b/c/d/", "/a/b/", "../.."}, + {"/../../a/b", "/../../a/b/c/d", "c/d"}, + {".", "a/b", "a/b"}, + {".", "..", ".."}, + + // can't do purely lexically + {"..", ".", "err"}, + {"..", "a", "err"}, + {"../..", "..", "err"}, + {"a", "/a", "err"}, + {"/a", "a", "err"}, +} + +var winreltests = []RelTests{ + {`C:a\b\c`, `C:a/b/d`, `..\d`}, + {`C:\`, `D:\`, `err`}, + {`C:`, `D:`, `err`}, + {`C:\Projects`, `c:\projects\src`, `src`}, + {`C:\Projects`, `c:\projects`, `.`}, + {`C:\Projects\a\..`, `c:\projects`, `.`}, + {`\\host\share`, `\\host\share\file.txt`, `file.txt`}, +} + +func TestRel(t *testing.T) { + tests := append([]RelTests{}, reltests...) + if runtime.GOOS == "windows" { + for i := range tests { + tests[i].want = filepath.FromSlash(tests[i].want) + } + tests = append(tests, winreltests...) + } + for _, test := range tests { + got, err := filepath.Rel(test.root, test.path) + if test.want == "err" { + if err == nil { + t.Errorf("Rel(%q, %q)=%q, want error", test.root, test.path, got) + } + continue + } + if err != nil { + t.Errorf("Rel(%q, %q): want %q, got error: %s", test.root, test.path, test.want, err) + } + if got != test.want { + t.Errorf("Rel(%q, %q)=%q, want %q", test.root, test.path, got, test.want) + } + } +} + +type VolumeNameTest struct { + path string + vol string +} + +var volumenametests = []VolumeNameTest{ + {`c:/foo/bar`, `c:`}, + {`c:`, `c:`}, + {`c:\`, `c:`}, + {`2:`, `2:`}, + {``, ``}, + {`\\\host`, `\\\host`}, + {`\\\host\`, `\\\host`}, + {`\\\host\share`, `\\\host`}, + {`\\\host\\share`, `\\\host`}, + {`\\host`, `\\host`}, + {`//host`, `\\host`}, + {`\\host\`, `\\host\`}, + {`//host/`, `\\host\`}, + {`\\host\share`, `\\host\share`}, + {`//host/share`, `\\host\share`}, + {`\\host\share\`, `\\host\share`}, + {`//host/share/`, `\\host\share`}, + {`\\host\share\foo`, `\\host\share`}, + {`//host/share/foo`, `\\host\share`}, + {`\\host\share\\foo\\\bar\\\\baz`, `\\host\share`}, + {`//host/share//foo///bar////baz`, `\\host\share`}, + {`\\host\share\foo\..\bar`, `\\host\share`}, + {`//host/share/foo/../bar`, `\\host\share`}, + {`//.`, `\\.`}, + {`//./`, `\\.\`}, + {`//./NUL`, `\\.\NUL`}, + {`//?`, `\\?`}, + {`//?/`, `\\?\`}, + {`//?/NUL`, `\\?\NUL`}, + {`/??`, `\??`}, + {`/??/`, `\??\`}, + {`/??/NUL`, `\??\NUL`}, + {`//./a/b`, `\\.\a`}, + {`//./C:`, `\\.\C:`}, + {`//./C:/`, `\\.\C:`}, + {`//./C:/a/b/c`, `\\.\C:`}, + {`//./UNC/host/share/a/b/c`, `\\.\UNC\host\share`}, + {`//./UNC/host`, `\\.\UNC\host`}, + {`//./UNC/host\`, `\\.\UNC\host\`}, + {`//./UNC`, `\\.\UNC`}, + {`//./UNC/`, `\\.\UNC\`}, + {`\\?\x`, `\\?\x`}, + {`\??\x`, `\??\x`}, +} + +func TestVolumeName(t *testing.T) { + if runtime.GOOS != "windows" { + return + } + for _, v := range volumenametests { + if vol := filepath.VolumeName(v.path); vol != v.vol { + t.Errorf("VolumeName(%q)=%q, want %q", v.path, vol, v.vol) + } + } +} + +func TestDriveLetterInEvalSymlinks(t *testing.T) { + if runtime.GOOS != "windows" { + return + } + wd, _ := os.Getwd() + if len(wd) < 3 { + t.Errorf("Current directory path %q is too short", wd) + } + lp := strings.ToLower(wd) + up := strings.ToUpper(wd) + flp, err := filepath.EvalSymlinks(lp) + if err != nil { + t.Fatalf("EvalSymlinks(%q) failed: %q", lp, err) + } + fup, err := filepath.EvalSymlinks(up) + if err != nil { + t.Fatalf("EvalSymlinks(%q) failed: %q", up, err) + } + if flp != fup { + t.Errorf("Results of EvalSymlinks do not match: %q and %q", flp, fup) + } +} + +func TestBug3486(t *testing.T) { // https://golang.org/issue/3486 + if runtime.GOOS == "ios" { + t.Skipf("skipping on %s/%s", runtime.GOOS, runtime.GOARCH) + } + root := filepath.Join(testenv.GOROOT(t), "src", "unicode") + utf16 := filepath.Join(root, "utf16") + utf8 := filepath.Join(root, "utf8") + seenUTF16 := false + seenUTF8 := false + err := filepath.Walk(root, func(pth string, info fs.FileInfo, err error) error { + if err != nil { + t.Fatal(err) + } + + switch pth { + case utf16: + seenUTF16 = true + return filepath.SkipDir + case utf8: + if !seenUTF16 { + t.Fatal("filepath.Walk out of order - utf8 before utf16") + } + seenUTF8 = true + } + return nil + }) + if err != nil { + t.Fatal(err) + } + if !seenUTF8 { + t.Fatalf("%q not seen", utf8) + } +} + +func testWalkSymlink(t *testing.T, mklink func(target, link string) error) { + tmpdir := t.TempDir() + + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer os.Chdir(wd) + + err = os.Chdir(tmpdir) + if err != nil { + t.Fatal(err) + } + + err = mklink(tmpdir, "link") + if err != nil { + t.Fatal(err) + } + + var visited []string + err = filepath.Walk(tmpdir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + t.Fatal(err) + } + rel, err := filepath.Rel(tmpdir, path) + if err != nil { + t.Fatal(err) + } + visited = append(visited, rel) + return nil + }) + if err != nil { + t.Fatal(err) + } + sort.Strings(visited) + want := []string{".", "link"} + if fmt.Sprintf("%q", visited) != fmt.Sprintf("%q", want) { + t.Errorf("unexpected paths visited %q, want %q", visited, want) + } +} + +func TestWalkSymlink(t *testing.T) { + testenv.MustHaveSymlink(t) + testWalkSymlink(t, os.Symlink) +} + +func TestIssue29372(t *testing.T) { + tmpDir := t.TempDir() + + path := filepath.Join(tmpDir, "file.txt") + err := os.WriteFile(path, nil, 0644) + if err != nil { + t.Fatal(err) + } + + pathSeparator := string(filepath.Separator) + tests := []string{ + path + strings.Repeat(pathSeparator, 1), + path + strings.Repeat(pathSeparator, 2), + path + strings.Repeat(pathSeparator, 1) + ".", + path + strings.Repeat(pathSeparator, 2) + ".", + path + strings.Repeat(pathSeparator, 1) + "..", + path + strings.Repeat(pathSeparator, 2) + "..", + } + + for i, test := range tests { + _, err = filepath.EvalSymlinks(test) + if err != syscall.ENOTDIR { + t.Fatalf("test#%d: want %q, got %q", i, syscall.ENOTDIR, err) + } + } +} + +// Issue 30520 part 1. +func TestEvalSymlinksAboveRoot(t *testing.T) { + testenv.MustHaveSymlink(t) + + t.Parallel() + + tmpDir := t.TempDir() + + evalTmpDir, err := filepath.EvalSymlinks(tmpDir) + if err != nil { + t.Fatal(err) + } + + if err := os.Mkdir(filepath.Join(evalTmpDir, "a"), 0777); err != nil { + t.Fatal(err) + } + if err := os.Symlink(filepath.Join(evalTmpDir, "a"), filepath.Join(evalTmpDir, "b")); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(evalTmpDir, "a", "file"), nil, 0666); err != nil { + t.Fatal(err) + } + + // Count the number of ".." elements to get to the root directory. + vol := filepath.VolumeName(evalTmpDir) + c := strings.Count(evalTmpDir[len(vol):], string(os.PathSeparator)) + var dd []string + for i := 0; i < c+2; i++ { + dd = append(dd, "..") + } + + wantSuffix := strings.Join([]string{"a", "file"}, string(os.PathSeparator)) + + // Try different numbers of "..". + for _, i := range []int{c, c + 1, c + 2} { + check := strings.Join([]string{evalTmpDir, strings.Join(dd[:i], string(os.PathSeparator)), evalTmpDir[len(vol)+1:], "b", "file"}, string(os.PathSeparator)) + resolved, err := filepath.EvalSymlinks(check) + switch { + case runtime.GOOS == "darwin" && errors.Is(err, fs.ErrNotExist): + // On darwin, the temp dir is sometimes cleaned up mid-test (issue 37910). + testenv.SkipFlaky(t, 37910) + case err != nil: + t.Errorf("EvalSymlinks(%q) failed: %v", check, err) + case !strings.HasSuffix(resolved, wantSuffix): + t.Errorf("EvalSymlinks(%q) = %q does not end with %q", check, resolved, wantSuffix) + default: + t.Logf("EvalSymlinks(%q) = %q", check, resolved) + } + } +} + +// Issue 30520 part 2. +func TestEvalSymlinksAboveRootChdir(t *testing.T) { + testenv.MustHaveSymlink(t) + + tmpDir, err := os.MkdirTemp("", "TestEvalSymlinksAboveRootChdir") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + chdir(t, tmpDir) + + subdir := filepath.Join("a", "b") + if err := os.MkdirAll(subdir, 0777); err != nil { + t.Fatal(err) + } + if err := os.Symlink(subdir, "c"); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(subdir, "file"), nil, 0666); err != nil { + t.Fatal(err) + } + + subdir = filepath.Join("d", "e", "f") + if err := os.MkdirAll(subdir, 0777); err != nil { + t.Fatal(err) + } + if err := os.Chdir(subdir); err != nil { + t.Fatal(err) + } + + check := filepath.Join("..", "..", "..", "c", "file") + wantSuffix := filepath.Join("a", "b", "file") + if resolved, err := filepath.EvalSymlinks(check); err != nil { + t.Errorf("EvalSymlinks(%q) failed: %v", check, err) + } else if !strings.HasSuffix(resolved, wantSuffix) { + t.Errorf("EvalSymlinks(%q) = %q does not end with %q", check, resolved, wantSuffix) + } else { + t.Logf("EvalSymlinks(%q) = %q", check, resolved) + } +} + +func TestIssue51617(t *testing.T) { + dir := t.TempDir() + for _, sub := range []string{"a", filepath.Join("a", "bad"), filepath.Join("a", "next")} { + if err := os.Mkdir(filepath.Join(dir, sub), 0755); err != nil { + t.Fatal(err) + } + } + bad := filepath.Join(dir, "a", "bad") + if err := os.Chmod(bad, 0); err != nil { + t.Fatal(err) + } + defer os.Chmod(bad, 0700) // avoid errors on cleanup + var saw []string + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return filepath.SkipDir + } + if d.IsDir() { + rel, err := filepath.Rel(dir, path) + if err != nil { + t.Fatal(err) + } + saw = append(saw, rel) + } + return nil + }) + if err != nil { + t.Fatal(err) + } + want := []string{".", "a", filepath.Join("a", "bad"), filepath.Join("a", "next")} + if !reflect.DeepEqual(saw, want) { + t.Errorf("got directories %v, want %v", saw, want) + } +} + +func TestEscaping(t *testing.T) { + dir1 := t.TempDir() + dir2 := t.TempDir() + chdir(t, dir1) + + for _, p := range []string{ + filepath.Join(dir2, "x"), + } { + if !filepath.IsLocal(p) { + continue + } + f, err := os.Create(p) + if err != nil { + f.Close() + } + ents, err := os.ReadDir(dir2) + if err != nil { + t.Fatal(err) + } + for _, e := range ents { + t.Fatalf("found: %v", e.Name()) + } + } +} diff --git a/src/path/filepath/path_unix.go b/src/path/filepath/path_unix.go new file mode 100644 index 0000000..57e6217 --- /dev/null +++ b/src/path/filepath/path_unix.go @@ -0,0 +1,57 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix || (js && wasm) || wasip1 + +package filepath + +import "strings" + +func isLocal(path string) bool { + return unixIsLocal(path) +} + +// IsAbs reports whether the path is absolute. +func IsAbs(path string) bool { + return strings.HasPrefix(path, "/") +} + +// volumeNameLen returns length of the leading volume name on Windows. +// It returns 0 elsewhere. +func volumeNameLen(path string) int { + return 0 +} + +// HasPrefix exists for historical compatibility and should not be used. +// +// Deprecated: HasPrefix does not respect path boundaries and +// does not ignore case when required. +func HasPrefix(p, prefix string) bool { + return strings.HasPrefix(p, prefix) +} + +func splitList(path string) []string { + if path == "" { + return []string{} + } + return strings.Split(path, string(ListSeparator)) +} + +func abs(path string) (string, error) { + return unixAbs(path) +} + +func join(elem []string) string { + // If there's a bug here, fix the logic in ./path_plan9.go too. + for i, e := range elem { + if e != "" { + return Clean(strings.Join(elem[i:], string(Separator))) + } + } + return "" +} + +func sameWord(a, b string) bool { + return a == b +} diff --git a/src/path/filepath/path_windows.go b/src/path/filepath/path_windows.go new file mode 100644 index 0000000..eacab0e --- /dev/null +++ b/src/path/filepath/path_windows.go @@ -0,0 +1,348 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filepath + +import ( + "internal/safefilepath" + "os" + "strings" + "syscall" +) + +func isSlash(c uint8) bool { + return c == '\\' || c == '/' +} + +func toUpper(c byte) byte { + if 'a' <= c && c <= 'z' { + return c - ('a' - 'A') + } + return c +} + +func isLocal(path string) bool { + if path == "" { + return false + } + if isSlash(path[0]) { + // Path rooted in the current drive. + return false + } + if strings.IndexByte(path, ':') >= 0 { + // Colons are only valid when marking a drive letter ("C:foo"). + // Rejecting any path with a colon is conservative but safe. + return false + } + hasDots := false // contains . or .. path elements + for p := path; p != ""; { + var part string + part, p, _ = cutPath(p) + if part == "." || part == ".." { + hasDots = true + } + if safefilepath.IsReservedName(part) { + return false + } + } + if hasDots { + path = Clean(path) + } + if path == ".." || strings.HasPrefix(path, `..\`) { + return false + } + return true +} + +// IsAbs reports whether the path is absolute. +func IsAbs(path string) (b bool) { + l := volumeNameLen(path) + if l == 0 { + return false + } + // If the volume name starts with a double slash, this is an absolute path. + if isSlash(path[0]) && isSlash(path[1]) { + return true + } + path = path[l:] + if path == "" { + return false + } + return isSlash(path[0]) +} + +// volumeNameLen returns length of the leading volume name on Windows. +// It returns 0 elsewhere. +// +// See: +// https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats +// https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html +func volumeNameLen(path string) int { + switch { + case len(path) >= 2 && path[1] == ':': + // Path starts with a drive letter. + // + // Not all Windows functions necessarily enforce the requirement that + // drive letters be in the set A-Z, and we don't try to here. + // + // We don't handle the case of a path starting with a non-ASCII character, + // in which case the "drive letter" might be multiple bytes long. + return 2 + + case len(path) == 0 || !isSlash(path[0]): + // Path does not have a volume component. + return 0 + + case pathHasPrefixFold(path, `\\.\UNC`): + // We're going to treat the UNC host and share as part of the volume + // prefix for historical reasons, but this isn't really principled; + // Windows's own GetFullPathName will happily remove the first + // component of the path in this space, converting + // \\.\unc\a\b\..\c into \\.\unc\a\c. + return uncLen(path, len(`\\.\UNC\`)) + + case pathHasPrefixFold(path, `\\.`) || + pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`): + // Path starts with \\.\, and is a Local Device path; or + // path starts with \\?\ or \??\ and is a Root Local Device path. + // + // We treat the next component after the \\.\ prefix as + // part of the volume name, which means Clean(`\\?\c:\`) + // won't remove the trailing \. (See #64028.) + if len(path) == 3 { + return 3 // exactly \\. + } + _, rest, ok := cutPath(path[4:]) + if !ok { + return len(path) + } + return len(path) - len(rest) - 1 + + case len(path) >= 2 && isSlash(path[1]): + // Path starts with \\, and is a UNC path. + return uncLen(path, 2) + } + return 0 +} + +// pathHasPrefixFold tests whether the path s begins with prefix, +// ignoring case and treating all path separators as equivalent. +// If s is longer than prefix, then s[len(prefix)] must be a path separator. +func pathHasPrefixFold(s, prefix string) bool { + if len(s) < len(prefix) { + return false + } + for i := 0; i < len(prefix); i++ { + if isSlash(prefix[i]) { + if !isSlash(s[i]) { + return false + } + } else if toUpper(prefix[i]) != toUpper(s[i]) { + return false + } + } + if len(s) > len(prefix) && !isSlash(s[len(prefix)]) { + return false + } + return true +} + +// uncLen returns the length of the volume prefix of a UNC path. +// prefixLen is the prefix prior to the start of the UNC host; +// for example, for "//host/share", the prefixLen is len("//")==2. +func uncLen(path string, prefixLen int) int { + count := 0 + for i := prefixLen; i < len(path); i++ { + if isSlash(path[i]) { + count++ + if count == 2 { + return i + } + } + } + return len(path) +} + +// cutPath slices path around the first path separator. +func cutPath(path string) (before, after string, found bool) { + for i := range path { + if isSlash(path[i]) { + return path[:i], path[i+1:], true + } + } + return path, "", false +} + +// HasPrefix exists for historical compatibility and should not be used. +// +// Deprecated: HasPrefix does not respect path boundaries and +// does not ignore case when required. +func HasPrefix(p, prefix string) bool { + if strings.HasPrefix(p, prefix) { + return true + } + return strings.HasPrefix(strings.ToLower(p), strings.ToLower(prefix)) +} + +func splitList(path string) []string { + // The same implementation is used in LookPath in os/exec; + // consider changing os/exec when changing this. + + if path == "" { + return []string{} + } + + // Split path, respecting but preserving quotes. + list := []string{} + start := 0 + quo := false + for i := 0; i < len(path); i++ { + switch c := path[i]; { + case c == '"': + quo = !quo + case c == ListSeparator && !quo: + list = append(list, path[start:i]) + start = i + 1 + } + } + list = append(list, path[start:]) + + // Remove quotes. + for i, s := range list { + list[i] = strings.ReplaceAll(s, `"`, ``) + } + + return list +} + +func abs(path string) (string, error) { + if path == "" { + // syscall.FullPath returns an error on empty path, because it's not a valid path. + // To implement Abs behavior of returning working directory on empty string input, + // special-case empty path by changing it to "." path. See golang.org/issue/24441. + path = "." + } + fullPath, err := syscall.FullPath(path) + if err != nil { + return "", err + } + return Clean(fullPath), nil +} + +func join(elem []string) string { + var b strings.Builder + var lastChar byte + for _, e := range elem { + switch { + case b.Len() == 0: + // Add the first non-empty path element unchanged. + case isSlash(lastChar): + // If the path ends in a slash, strip any leading slashes from the next + // path element to avoid creating a UNC path (any path starting with "\\") + // from non-UNC elements. + // + // The correct behavior for Join when the first element is an incomplete UNC + // path (for example, "\\") is underspecified. We currently join subsequent + // elements so Join("\\", "host", "share") produces "\\host\share". + for len(e) > 0 && isSlash(e[0]) { + e = e[1:] + } + // If the path is \ and the next path element is ??, + // add an extra .\ to create \.\?? rather than \??\ + // (a Root Local Device path). + if b.Len() == 1 && pathHasPrefixFold(e, "??") { + b.WriteString(`.\`) + } + case lastChar == ':': + // If the path ends in a colon, keep the path relative to the current directory + // on a drive and don't add a separator. Preserve leading slashes in the next + // path element, which may make the path absolute. + // + // Join(`C:`, `f`) = `C:f` + // Join(`C:`, `\f`) = `C:\f` + default: + // In all other cases, add a separator between elements. + b.WriteByte('\\') + lastChar = '\\' + } + if len(e) > 0 { + b.WriteString(e) + lastChar = e[len(e)-1] + } + } + if b.Len() == 0 { + return "" + } + return Clean(b.String()) +} + +// joinNonEmpty is like join, but it assumes that the first element is non-empty. +func joinNonEmpty(elem []string) string { + if len(elem[0]) == 2 && elem[0][1] == ':' { + // First element is drive letter without terminating slash. + // Keep path relative to current directory on that drive. + // Skip empty elements. + i := 1 + for ; i < len(elem); i++ { + if elem[i] != "" { + break + } + } + return Clean(elem[0] + strings.Join(elem[i:], string(Separator))) + } + // The following logic prevents Join from inadvertently creating a + // UNC path on Windows. Unless the first element is a UNC path, Join + // shouldn't create a UNC path. See golang.org/issue/9167. + p := Clean(strings.Join(elem, string(Separator))) + if !isUNC(p) { + return p + } + // p == UNC only allowed when the first element is a UNC path. + head := Clean(elem[0]) + if isUNC(head) { + return p + } + // head + tail == UNC, but joining two non-UNC paths should not result + // in a UNC path. Undo creation of UNC path. + tail := Clean(strings.Join(elem[1:], string(Separator))) + if head[len(head)-1] == Separator { + return head + tail + } + return head + string(Separator) + tail +} + +// isUNC reports whether path is a UNC path. +func isUNC(path string) bool { + return len(path) > 1 && isSlash(path[0]) && isSlash(path[1]) +} + +func sameWord(a, b string) bool { + return strings.EqualFold(a, b) +} + +// postClean adjusts the results of Clean to avoid turning a relative path +// into an absolute or rooted one. +func postClean(out *lazybuf) { + if out.volLen != 0 || out.buf == nil { + return + } + // If a ':' appears in the path element at the start of a path, + // insert a .\ at the beginning to avoid converting relative paths + // like a/../c: into c:. + for _, c := range out.buf { + if os.IsPathSeparator(c) { + break + } + if c == ':' { + out.prepend('.', Separator) + return + } + } + // If a path begins with \??\, insert a \. at the beginning + // to avoid converting paths like \a\..\??\c:\x into \??\c:\x + // (equivalent to c:\x). + if len(out.buf) >= 3 && os.IsPathSeparator(out.buf[0]) && out.buf[1] == '?' && out.buf[2] == '?' { + out.prepend(Separator, '.') + } +} diff --git a/src/path/filepath/path_windows_test.go b/src/path/filepath/path_windows_test.go new file mode 100644 index 0000000..42aeb4f --- /dev/null +++ b/src/path/filepath/path_windows_test.go @@ -0,0 +1,588 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filepath_test + +import ( + "flag" + "fmt" + "internal/testenv" + "io/fs" + "os" + "os/exec" + "path/filepath" + "reflect" + "runtime/debug" + "strings" + "testing" +) + +func TestWinSplitListTestsAreValid(t *testing.T) { + comspec := os.Getenv("ComSpec") + if comspec == "" { + t.Fatal("%ComSpec% must be set") + } + + for ti, tt := range winsplitlisttests { + testWinSplitListTestIsValid(t, ti, tt, comspec) + } +} + +func testWinSplitListTestIsValid(t *testing.T, ti int, tt SplitListTest, + comspec string) { + + const ( + cmdfile = `printdir.cmd` + perm fs.FileMode = 0700 + ) + + tmp := t.TempDir() + for i, d := range tt.result { + if d == "" { + continue + } + if cd := filepath.Clean(d); filepath.VolumeName(cd) != "" || + cd[0] == '\\' || cd == ".." || (len(cd) >= 3 && cd[0:3] == `..\`) { + t.Errorf("%d,%d: %#q refers outside working directory", ti, i, d) + return + } + dd := filepath.Join(tmp, d) + if _, err := os.Stat(dd); err == nil { + t.Errorf("%d,%d: %#q already exists", ti, i, d) + return + } + if err := os.MkdirAll(dd, perm); err != nil { + t.Errorf("%d,%d: MkdirAll(%#q) failed: %v", ti, i, dd, err) + return + } + fn, data := filepath.Join(dd, cmdfile), []byte("@echo "+d+"\r\n") + if err := os.WriteFile(fn, data, perm); err != nil { + t.Errorf("%d,%d: WriteFile(%#q) failed: %v", ti, i, fn, err) + return + } + } + + // on some systems, SystemRoot is required for cmd to work + systemRoot := os.Getenv("SystemRoot") + + for i, d := range tt.result { + if d == "" { + continue + } + exp := []byte(d + "\r\n") + cmd := &exec.Cmd{ + Path: comspec, + Args: []string{`/c`, cmdfile}, + Env: []string{`Path=` + systemRoot + "/System32;" + tt.list, `SystemRoot=` + systemRoot}, + Dir: tmp, + } + out, err := cmd.CombinedOutput() + switch { + case err != nil: + t.Errorf("%d,%d: execution error %v\n%q", ti, i, err, out) + return + case !reflect.DeepEqual(out, exp): + t.Errorf("%d,%d: expected %#q, got %#q", ti, i, exp, out) + return + default: + // unshadow cmdfile in next directory + err = os.Remove(filepath.Join(tmp, d, cmdfile)) + if err != nil { + t.Fatalf("Remove test command failed: %v", err) + } + } + } +} + +func TestWindowsEvalSymlinks(t *testing.T) { + testenv.MustHaveSymlink(t) + + tmpDir := tempDirCanonical(t) + + if len(tmpDir) < 3 { + t.Fatalf("tmpDir path %q is too short", tmpDir) + } + if tmpDir[1] != ':' { + t.Fatalf("tmpDir path %q must have drive letter in it", tmpDir) + } + test := EvalSymlinksTest{"test/linkabswin", tmpDir[:3]} + + // Create the symlink farm using relative paths. + testdirs := append(EvalSymlinksTestDirs, test) + for _, d := range testdirs { + var err error + path := simpleJoin(tmpDir, d.path) + if d.dest == "" { + err = os.Mkdir(path, 0755) + } else { + err = os.Symlink(d.dest, path) + } + if err != nil { + t.Fatal(err) + } + } + + path := simpleJoin(tmpDir, test.path) + + testEvalSymlinks(t, path, test.dest) + + testEvalSymlinksAfterChdir(t, path, ".", test.dest) + + testEvalSymlinksAfterChdir(t, + path, + filepath.VolumeName(tmpDir)+".", + test.dest) + + testEvalSymlinksAfterChdir(t, + simpleJoin(tmpDir, "test"), + simpleJoin("..", test.path), + test.dest) + + testEvalSymlinksAfterChdir(t, tmpDir, test.path, test.dest) +} + +// TestEvalSymlinksCanonicalNames verify that EvalSymlinks +// returns "canonical" path names on windows. +func TestEvalSymlinksCanonicalNames(t *testing.T) { + ctmp := tempDirCanonical(t) + dirs := []string{ + "test", + "test/dir", + "testing_long_dir", + "TEST2", + } + + for _, d := range dirs { + dir := filepath.Join(ctmp, d) + err := os.Mkdir(dir, 0755) + if err != nil { + t.Fatal(err) + } + cname, err := filepath.EvalSymlinks(dir) + if err != nil { + t.Errorf("EvalSymlinks(%q) error: %v", dir, err) + continue + } + if dir != cname { + t.Errorf("EvalSymlinks(%q) returns %q, but should return %q", dir, cname, dir) + continue + } + // test non-canonical names + test := strings.ToUpper(dir) + p, err := filepath.EvalSymlinks(test) + if err != nil { + t.Errorf("EvalSymlinks(%q) error: %v", test, err) + continue + } + if p != cname { + t.Errorf("EvalSymlinks(%q) returns %q, but should return %q", test, p, cname) + continue + } + // another test + test = strings.ToLower(dir) + p, err = filepath.EvalSymlinks(test) + if err != nil { + t.Errorf("EvalSymlinks(%q) error: %v", test, err) + continue + } + if p != cname { + t.Errorf("EvalSymlinks(%q) returns %q, but should return %q", test, p, cname) + continue + } + } +} + +// checkVolume8dot3Setting runs "fsutil 8dot3name query c:" command +// (where c: is vol parameter) to discover "8dot3 name creation state". +// The state is combination of 2 flags. The global flag controls if it +// is per volume or global setting: +// +// 0 - Enable 8dot3 name creation on all volumes on the system +// 1 - Disable 8dot3 name creation on all volumes on the system +// 2 - Set 8dot3 name creation on a per volume basis +// 3 - Disable 8dot3 name creation on all volumes except the system volume +// +// If global flag is set to 2, then per-volume flag needs to be examined: +// +// 0 - Enable 8dot3 name creation on this volume +// 1 - Disable 8dot3 name creation on this volume +// +// checkVolume8dot3Setting verifies that "8dot3 name creation" flags +// are set to 2 and 0, if enabled parameter is true, or 2 and 1, if enabled +// is false. Otherwise checkVolume8dot3Setting returns error. +func checkVolume8dot3Setting(vol string, enabled bool) error { + // It appears, on some systems "fsutil 8dot3name query ..." command always + // exits with error. Ignore exit code, and look at fsutil output instead. + out, _ := exec.Command("fsutil", "8dot3name", "query", vol).CombinedOutput() + // Check that system has "Volume level setting" set. + expected := "The registry state of NtfsDisable8dot3NameCreation is 2, the default (Volume level setting)" + if !strings.Contains(string(out), expected) { + // Windows 10 version of fsutil has different output message. + expectedWindow10 := "The registry state is: 2 (Per volume setting - the default)" + if !strings.Contains(string(out), expectedWindow10) { + return fmt.Errorf("fsutil output should contain %q, but is %q", expected, string(out)) + } + } + // Now check the volume setting. + expected = "Based on the above two settings, 8dot3 name creation is %s on %s" + if enabled { + expected = fmt.Sprintf(expected, "enabled", vol) + } else { + expected = fmt.Sprintf(expected, "disabled", vol) + } + if !strings.Contains(string(out), expected) { + return fmt.Errorf("unexpected fsutil output: %q", string(out)) + } + return nil +} + +func setVolume8dot3Setting(vol string, enabled bool) error { + cmd := []string{"fsutil", "8dot3name", "set", vol} + if enabled { + cmd = append(cmd, "0") + } else { + cmd = append(cmd, "1") + } + // It appears, on some systems "fsutil 8dot3name set ..." command always + // exits with error. Ignore exit code, and look at fsutil output instead. + out, _ := exec.Command(cmd[0], cmd[1:]...).CombinedOutput() + if string(out) != "\r\nSuccessfully set 8dot3name behavior.\r\n" { + // Windows 10 version of fsutil has different output message. + expectedWindow10 := "Successfully %s 8dot3name generation on %s\r\n" + if enabled { + expectedWindow10 = fmt.Sprintf(expectedWindow10, "enabled", vol) + } else { + expectedWindow10 = fmt.Sprintf(expectedWindow10, "disabled", vol) + } + if string(out) != expectedWindow10 { + return fmt.Errorf("%v command failed: %q", cmd, string(out)) + } + } + return nil +} + +var runFSModifyTests = flag.Bool("run_fs_modify_tests", false, "run tests which modify filesystem parameters") + +// This test assumes registry state of NtfsDisable8dot3NameCreation is 2, +// the default (Volume level setting). +func TestEvalSymlinksCanonicalNamesWith8dot3Disabled(t *testing.T) { + if !*runFSModifyTests { + t.Skip("skipping test that modifies file system setting; enable with -run_fs_modify_tests") + } + tempVol := filepath.VolumeName(os.TempDir()) + if len(tempVol) != 2 { + t.Fatalf("unexpected temp volume name %q", tempVol) + } + + err := checkVolume8dot3Setting(tempVol, true) + if err != nil { + t.Fatal(err) + } + err = setVolume8dot3Setting(tempVol, false) + if err != nil { + t.Fatal(err) + } + defer func() { + err := setVolume8dot3Setting(tempVol, true) + if err != nil { + t.Fatal(err) + } + err = checkVolume8dot3Setting(tempVol, true) + if err != nil { + t.Fatal(err) + } + }() + err = checkVolume8dot3Setting(tempVol, false) + if err != nil { + t.Fatal(err) + } + TestEvalSymlinksCanonicalNames(t) +} + +func TestToNorm(t *testing.T) { + stubBase := func(path string) (string, error) { + vol := filepath.VolumeName(path) + path = path[len(vol):] + + if strings.Contains(path, "/") { + return "", fmt.Errorf("invalid path is given to base: %s", vol+path) + } + + if path == "" || path == "." || path == `\` { + return "", fmt.Errorf("invalid path is given to base: %s", vol+path) + } + + i := strings.LastIndexByte(path, filepath.Separator) + if i == len(path)-1 { // trailing '\' is invalid + return "", fmt.Errorf("invalid path is given to base: %s", vol+path) + } + if i == -1 { + return strings.ToUpper(path), nil + } + + return strings.ToUpper(path[i+1:]), nil + } + + // On this test, toNorm should be same as string.ToUpper(filepath.Clean(path)) except empty string. + tests := []struct { + arg string + want string + }{ + {"", ""}, + {".", "."}, + {"./foo/bar", `FOO\BAR`}, + {"/", `\`}, + {"/foo/bar", `\FOO\BAR`}, + {"/foo/bar/baz/qux", `\FOO\BAR\BAZ\QUX`}, + {"foo/bar", `FOO\BAR`}, + {"C:/foo/bar", `C:\FOO\BAR`}, + {"C:foo/bar", `C:FOO\BAR`}, + {"c:/foo/bar", `C:\FOO\BAR`}, + {"C:/foo/bar", `C:\FOO\BAR`}, + {"C:/foo/bar/", `C:\FOO\BAR`}, + {`C:\foo\bar`, `C:\FOO\BAR`}, + {`C:\foo/bar\`, `C:\FOO\BAR`}, + {"C:/ふー/バー", `C:\ふー\バー`}, + } + + for _, test := range tests { + var path string + if test.arg != "" { + path = filepath.Clean(test.arg) + } + got, err := filepath.ToNorm(path, stubBase) + if err != nil { + t.Errorf("toNorm(%s) failed: %v\n", test.arg, err) + } else if got != test.want { + t.Errorf("toNorm(%s) returns %s, but %s expected\n", test.arg, got, test.want) + } + } + + testPath := `{{tmp}}\test\foo\bar` + + testsDir := []struct { + wd string + arg string + want string + }{ + // test absolute paths + {".", `{{tmp}}\test\foo\bar`, `{{tmp}}\test\foo\bar`}, + {".", `{{tmp}}\.\test/foo\bar`, `{{tmp}}\test\foo\bar`}, + {".", `{{tmp}}\test\..\test\foo\bar`, `{{tmp}}\test\foo\bar`}, + {".", `{{tmp}}\TEST\FOO\BAR`, `{{tmp}}\test\foo\bar`}, + + // test relative paths begin with drive letter + {`{{tmp}}\test`, `{{tmpvol}}.`, `{{tmpvol}}.`}, + {`{{tmp}}\test`, `{{tmpvol}}..`, `{{tmpvol}}..`}, + {`{{tmp}}\test`, `{{tmpvol}}foo\bar`, `{{tmpvol}}foo\bar`}, + {`{{tmp}}\test`, `{{tmpvol}}.\foo\bar`, `{{tmpvol}}foo\bar`}, + {`{{tmp}}\test`, `{{tmpvol}}foo\..\foo\bar`, `{{tmpvol}}foo\bar`}, + {`{{tmp}}\test`, `{{tmpvol}}FOO\BAR`, `{{tmpvol}}foo\bar`}, + + // test relative paths begin with '\' + {"{{tmp}}", `{{tmpnovol}}\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`}, + {"{{tmp}}", `{{tmpnovol}}\.\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`}, + {"{{tmp}}", `{{tmpnovol}}\test\..\test\foo\bar`, `{{tmpnovol}}\test\foo\bar`}, + {"{{tmp}}", `{{tmpnovol}}\TEST\FOO\BAR`, `{{tmpnovol}}\test\foo\bar`}, + + // test relative paths begin without '\' + {`{{tmp}}\test`, ".", `.`}, + {`{{tmp}}\test`, "..", `..`}, + {`{{tmp}}\test`, `foo\bar`, `foo\bar`}, + {`{{tmp}}\test`, `.\foo\bar`, `foo\bar`}, + {`{{tmp}}\test`, `foo\..\foo\bar`, `foo\bar`}, + {`{{tmp}}\test`, `FOO\BAR`, `foo\bar`}, + + // test UNC paths + {".", `\\localhost\c$`, `\\localhost\c$`}, + } + + ctmp := tempDirCanonical(t) + if err := os.MkdirAll(strings.ReplaceAll(testPath, "{{tmp}}", ctmp), 0777); err != nil { + t.Fatal(err) + } + + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer func() { + err := os.Chdir(cwd) + if err != nil { + t.Fatal(err) + } + }() + + tmpVol := filepath.VolumeName(ctmp) + if len(tmpVol) != 2 { + t.Fatalf("unexpected temp volume name %q", tmpVol) + } + + tmpNoVol := ctmp[len(tmpVol):] + + replacer := strings.NewReplacer("{{tmp}}", ctmp, "{{tmpvol}}", tmpVol, "{{tmpnovol}}", tmpNoVol) + + for _, test := range testsDir { + wd := replacer.Replace(test.wd) + arg := replacer.Replace(test.arg) + want := replacer.Replace(test.want) + + if test.wd == "." { + err := os.Chdir(cwd) + if err != nil { + t.Error(err) + + continue + } + } else { + err := os.Chdir(wd) + if err != nil { + t.Error(err) + + continue + } + } + if arg != "" { + arg = filepath.Clean(arg) + } + got, err := filepath.ToNorm(arg, filepath.NormBase) + if err != nil { + t.Errorf("toNorm(%s) failed: %v (wd=%s)\n", arg, err, wd) + } else if got != want { + t.Errorf("toNorm(%s) returns %s, but %s expected (wd=%s)\n", arg, got, want, wd) + } + } +} + +func TestUNC(t *testing.T) { + // Test that this doesn't go into an infinite recursion. + // See golang.org/issue/15879. + defer debug.SetMaxStack(debug.SetMaxStack(1e6)) + filepath.Glob(`\\?\c:\*`) +} + +func testWalkMklink(t *testing.T, linktype string) { + output, _ := exec.Command("cmd", "/c", "mklink", "/?").Output() + if !strings.Contains(string(output), fmt.Sprintf(" /%s ", linktype)) { + t.Skipf(`skipping test; mklink does not supports /%s parameter`, linktype) + } + testWalkSymlink(t, func(target, link string) error { + output, err := exec.Command("cmd", "/c", "mklink", "/"+linktype, link, target).CombinedOutput() + if err != nil { + return fmt.Errorf(`"mklink /%s %v %v" command failed: %v\n%v`, linktype, link, target, err, string(output)) + } + return nil + }) +} + +func TestWalkDirectoryJunction(t *testing.T) { + testenv.MustHaveSymlink(t) + testWalkMklink(t, "J") +} + +func TestWalkDirectorySymlink(t *testing.T) { + testenv.MustHaveSymlink(t) + testWalkMklink(t, "D") +} + +func TestNTNamespaceSymlink(t *testing.T) { + output, _ := exec.Command("cmd", "/c", "mklink", "/?").Output() + if !strings.Contains(string(output), " /J ") { + t.Skip("skipping test because mklink command does not support junctions") + } + + tmpdir := tempDirCanonical(t) + + vol := filepath.VolumeName(tmpdir) + output, err := exec.Command("cmd", "/c", "mountvol", vol, "/L").CombinedOutput() + if err != nil { + t.Fatalf("failed to run mountvol %v /L: %v %q", vol, err, output) + } + target := strings.Trim(string(output), " \n\r") + + dirlink := filepath.Join(tmpdir, "dirlink") + output, err = exec.Command("cmd", "/c", "mklink", "/J", dirlink, target).CombinedOutput() + if err != nil { + t.Fatalf("failed to run mklink %v %v: %v %q", dirlink, target, err, output) + } + + got, err := filepath.EvalSymlinks(dirlink) + if err != nil { + t.Fatal(err) + } + if want := vol + `\`; got != want { + t.Errorf(`EvalSymlinks(%q): got %q, want %q`, dirlink, got, want) + } + + // Make sure we have sufficient privilege to run mklink command. + testenv.MustHaveSymlink(t) + + file := filepath.Join(tmpdir, "file") + err = os.WriteFile(file, []byte(""), 0666) + if err != nil { + t.Fatal(err) + } + + target += file[len(filepath.VolumeName(file)):] + + filelink := filepath.Join(tmpdir, "filelink") + output, err = exec.Command("cmd", "/c", "mklink", filelink, target).CombinedOutput() + if err != nil { + t.Fatalf("failed to run mklink %v %v: %v %q", filelink, target, err, output) + } + + got, err = filepath.EvalSymlinks(filelink) + if err != nil { + t.Fatal(err) + } + if want := file; got != want { + t.Errorf(`EvalSymlinks(%q): got %q, want %q`, filelink, got, want) + } +} + +func TestIssue52476(t *testing.T) { + tests := []struct { + lhs, rhs string + want string + }{ + {`..\.`, `C:`, `..\C:`}, + {`..`, `C:`, `..\C:`}, + {`.`, `:`, `.\:`}, + {`.`, `C:`, `.\C:`}, + {`.`, `C:/a/b/../c`, `.\C:\a\c`}, + {`.`, `\C:`, `.\C:`}, + {`C:\`, `.`, `C:\`}, + {`C:\`, `C:\`, `C:\C:`}, + {`C`, `:`, `C\:`}, + {`\.`, `C:`, `\C:`}, + {`\`, `C:`, `\C:`}, + } + + for _, test := range tests { + got := filepath.Join(test.lhs, test.rhs) + if got != test.want { + t.Errorf(`Join(%q, %q): got %q, want %q`, test.lhs, test.rhs, got, test.want) + } + } +} + +func TestAbsWindows(t *testing.T) { + for _, test := range []struct { + path string + want string + }{ + {`C:\foo`, `C:\foo`}, + {`\\host\share\foo`, `\\host\share\foo`}, + {`\\host`, `\\host`}, + {`\\.\NUL`, `\\.\NUL`}, + {`NUL`, `\\.\NUL`}, + {`COM1`, `\\.\COM1`}, + {`a/NUL`, `\\.\NUL`}, + } { + got, err := filepath.Abs(test.path) + if err != nil || got != test.want { + t.Errorf("Abs(%q) = %q, %v; want %q, nil", test.path, got, err, test.want) + } + } +} diff --git a/src/path/filepath/symlink.go b/src/path/filepath/symlink.go new file mode 100644 index 0000000..f9435e0 --- /dev/null +++ b/src/path/filepath/symlink.go @@ -0,0 +1,149 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filepath + +import ( + "errors" + "io/fs" + "os" + "runtime" + "syscall" +) + +func walkSymlinks(path string) (string, error) { + volLen := volumeNameLen(path) + pathSeparator := string(os.PathSeparator) + + if volLen < len(path) && os.IsPathSeparator(path[volLen]) { + volLen++ + } + vol := path[:volLen] + dest := vol + linksWalked := 0 + for start, end := volLen, volLen; start < len(path); start = end { + for start < len(path) && os.IsPathSeparator(path[start]) { + start++ + } + end = start + for end < len(path) && !os.IsPathSeparator(path[end]) { + end++ + } + + // On Windows, "." can be a symlink. + // We look it up, and use the value if it is absolute. + // If not, we just return ".". + isWindowsDot := runtime.GOOS == "windows" && path[volumeNameLen(path):] == "." + + // The next path component is in path[start:end]. + if end == start { + // No more path components. + break + } else if path[start:end] == "." && !isWindowsDot { + // Ignore path component ".". + continue + } else if path[start:end] == ".." { + // Back up to previous component if possible. + // Note that volLen includes any leading slash. + + // Set r to the index of the last slash in dest, + // after the volume. + var r int + for r = len(dest) - 1; r >= volLen; r-- { + if os.IsPathSeparator(dest[r]) { + break + } + } + if r < volLen || dest[r+1:] == ".." { + // Either path has no slashes + // (it's empty or just "C:") + // or it ends in a ".." we had to keep. + // Either way, keep this "..". + if len(dest) > volLen { + dest += pathSeparator + } + dest += ".." + } else { + // Discard everything since the last slash. + dest = dest[:r] + } + continue + } + + // Ordinary path component. Add it to result. + + if len(dest) > volumeNameLen(dest) && !os.IsPathSeparator(dest[len(dest)-1]) { + dest += pathSeparator + } + + dest += path[start:end] + + // Resolve symlink. + + fi, err := os.Lstat(dest) + if err != nil { + return "", err + } + + if fi.Mode()&fs.ModeSymlink == 0 { + if !fi.Mode().IsDir() && end < len(path) { + return "", syscall.ENOTDIR + } + continue + } + + // Found symlink. + + linksWalked++ + if linksWalked > 255 { + return "", errors.New("EvalSymlinks: too many links") + } + + link, err := os.Readlink(dest) + if err != nil { + return "", err + } + + if isWindowsDot && !IsAbs(link) { + // On Windows, if "." is a relative symlink, + // just return ".". + break + } + + path = link + path[end:] + + v := volumeNameLen(link) + if v > 0 { + // Symlink to drive name is an absolute path. + if v < len(link) && os.IsPathSeparator(link[v]) { + v++ + } + vol = link[:v] + dest = vol + end = len(vol) + } else if len(link) > 0 && os.IsPathSeparator(link[0]) { + // Symlink to absolute path. + dest = link[:1] + end = 1 + vol = link[:1] + volLen = 1 + } else { + // Symlink to relative path; replace last + // path component in dest. + var r int + for r = len(dest) - 1; r >= volLen; r-- { + if os.IsPathSeparator(dest[r]) { + break + } + } + if r < volLen { + dest = vol + } else { + dest = dest[:r] + } + end = 0 + } + } + return Clean(dest), nil +} diff --git a/src/path/filepath/symlink_plan9.go b/src/path/filepath/symlink_plan9.go new file mode 100644 index 0000000..820d150 --- /dev/null +++ b/src/path/filepath/symlink_plan9.go @@ -0,0 +1,27 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filepath + +import ( + "os" + "strings" + "syscall" +) + +func evalSymlinks(path string) (string, error) { + // Plan 9 doesn't have symbolic links, so no need for substitutions. + if len(path) > 0 { + // Check validity of path + _, err := os.Lstat(path) + if err != nil { + // Return the same error value as on other operating systems + if strings.HasSuffix(err.Error(), "not a directory") { + err = syscall.ENOTDIR + } + return "", err + } + } + return Clean(path), nil +} diff --git a/src/path/filepath/symlink_unix.go b/src/path/filepath/symlink_unix.go new file mode 100644 index 0000000..4cac063 --- /dev/null +++ b/src/path/filepath/symlink_unix.go @@ -0,0 +1,11 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !windows && !plan9 + +package filepath + +func evalSymlinks(path string) (string, error) { + return walkSymlinks(path) +} diff --git a/src/path/filepath/symlink_windows.go b/src/path/filepath/symlink_windows.go new file mode 100644 index 0000000..8047ff8 --- /dev/null +++ b/src/path/filepath/symlink_windows.go @@ -0,0 +1,118 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filepath + +import ( + "strings" + "syscall" +) + +// normVolumeName is like VolumeName, but makes drive letter upper case. +// result of EvalSymlinks must be unique, so we have +// EvalSymlinks(`c:\a`) == EvalSymlinks(`C:\a`). +func normVolumeName(path string) string { + volume := VolumeName(path) + + if len(volume) > 2 { // isUNC + return volume + } + + return strings.ToUpper(volume) +} + +// normBase returns the last element of path with correct case. +func normBase(path string) (string, error) { + p, err := syscall.UTF16PtrFromString(path) + if err != nil { + return "", err + } + + var data syscall.Win32finddata + + h, err := syscall.FindFirstFile(p, &data) + if err != nil { + return "", err + } + syscall.FindClose(h) + + return syscall.UTF16ToString(data.FileName[:]), nil +} + +// baseIsDotDot reports whether the last element of path is "..". +// The given path should be 'Clean'-ed in advance. +func baseIsDotDot(path string) bool { + i := strings.LastIndexByte(path, Separator) + return path[i+1:] == ".." +} + +// toNorm returns the normalized path that is guaranteed to be unique. +// It should accept the following formats: +// - UNC paths (e.g \\server\share\foo\bar) +// - absolute paths (e.g C:\foo\bar) +// - relative paths begin with drive letter (e.g C:foo\bar, C:..\foo\bar, C:.., C:.) +// - relative paths begin with '\' (e.g \foo\bar) +// - relative paths begin without '\' (e.g foo\bar, ..\foo\bar, .., .) +// +// The returned normalized path will be in the same form (of 5 listed above) as the input path. +// If two paths A and B are indicating the same file with the same format, toNorm(A) should be equal to toNorm(B). +// The normBase parameter should be equal to the normBase func, except for in tests. See docs on the normBase func. +func toNorm(path string, normBase func(string) (string, error)) (string, error) { + if path == "" { + return path, nil + } + + volume := normVolumeName(path) + path = path[len(volume):] + + // skip special cases + if path == "" || path == "." || path == `\` { + return volume + path, nil + } + + var normPath string + + for { + if baseIsDotDot(path) { + normPath = path + `\` + normPath + + break + } + + name, err := normBase(volume + path) + if err != nil { + return "", err + } + + normPath = name + `\` + normPath + + i := strings.LastIndexByte(path, Separator) + if i == -1 { + break + } + if i == 0 { // `\Go` or `C:\Go` + normPath = `\` + normPath + + break + } + + path = path[:i] + } + + normPath = normPath[:len(normPath)-1] // remove trailing '\' + + return volume + normPath, nil +} + +func evalSymlinks(path string) (string, error) { + newpath, err := walkSymlinks(path) + if err != nil { + return "", err + } + newpath, err = toNorm(newpath, normBase) + if err != nil { + return "", err + } + return newpath, nil +} |