Initial commit: Go 1.23 release state

This commit is contained in:
Vorapol Rinsatitnon
2024-09-21 23:49:08 +10:00
commit 17cd57a668
13231 changed files with 3114330 additions and 0 deletions

121
src/path/example_test.go Normal file
View File

@@ -0,0 +1,121 @@
// 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 path_test
import (
"fmt"
"path"
)
func ExampleBase() {
fmt.Println(path.Base("/a/b"))
fmt.Println(path.Base("/"))
fmt.Println(path.Base(""))
// Output:
// b
// /
// .
}
func ExampleClean() {
paths := []string{
"a/c",
"a//c",
"a/c/.",
"a/c/b/..",
"/../a/c",
"/../a/b/../././/c",
"",
}
for _, p := range paths {
fmt.Printf("Clean(%q) = %q\n", p, path.Clean(p))
}
// Output:
// Clean("a/c") = "a/c"
// Clean("a//c") = "a/c"
// Clean("a/c/.") = "a/c"
// Clean("a/c/b/..") = "a/c"
// Clean("/../a/c") = "/a/c"
// Clean("/../a/b/../././/c") = "/a/c"
// Clean("") = "."
}
func ExampleDir() {
fmt.Println(path.Dir("/a/b/c"))
fmt.Println(path.Dir("a/b/c"))
fmt.Println(path.Dir("/a/"))
fmt.Println(path.Dir("a/"))
fmt.Println(path.Dir("/"))
fmt.Println(path.Dir(""))
// Output:
// /a/b
// a/b
// /a
// a
// /
// .
}
func ExampleExt() {
fmt.Println(path.Ext("/a/b/c/bar.css"))
fmt.Println(path.Ext("/"))
fmt.Println(path.Ext(""))
// Output:
// .css
//
//
}
func ExampleIsAbs() {
fmt.Println(path.IsAbs("/dev/null"))
// Output: true
}
func ExampleJoin() {
fmt.Println(path.Join("a", "b", "c"))
fmt.Println(path.Join("a", "b/c"))
fmt.Println(path.Join("a/b", "c"))
fmt.Println(path.Join("a/b", "../../../xyz"))
fmt.Println(path.Join("", ""))
fmt.Println(path.Join("a", ""))
fmt.Println(path.Join("", "a"))
// Output:
// a/b/c
// a/b/c
// a/b/c
// ../xyz
//
// a
// a
}
func ExampleMatch() {
fmt.Println(path.Match("abc", "abc"))
fmt.Println(path.Match("a*", "abc"))
fmt.Println(path.Match("a*/b", "a/c/b"))
// Output:
// true <nil>
// true <nil>
// false <nil>
}
func ExampleSplit() {
split := func(s string) {
dir, file := path.Split(s)
fmt.Printf("path.Split(%q) = dir: %q, file: %q\n", s, dir, file)
}
split("static/myfile.css")
split("myfile.css")
split("")
// Output:
// path.Split("static/myfile.css") = dir: "static/", file: "myfile.css"
// path.Split("myfile.css") = dir: "", file: "myfile.css"
// path.Split("") = dir: "", file: ""
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
)

370
src/path/filepath/match.go Normal file
View File

@@ -0,0 +1,370 @@
// 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"
"internal/filepathlite"
"os"
"runtime"
"slices"
"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 := filepathlite.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)
slices.Sort(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)
}

View File

@@ -0,0 +1,373 @@
// 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"
"slices"
"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))
}
}
}
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 !slices.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 !slices.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))
}
slices.Sort(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
}
slices.Sort(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
}
slices.Sort(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)
}
}

475
src/path/filepath/path.go Normal file
View File

@@ -0,0 +1,475 @@
// 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"
"internal/bytealg"
"internal/filepathlite"
"io/fs"
"os"
"slices"
)
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 {
return filepathlite.Clean(path)
}
// 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 filepathlite.IsLocal(path)
}
// Localize converts a slash-separated path into an operating system path.
// The input path must be a valid path as reported by [io/fs.ValidPath].
//
// Localize returns an error if the path cannot be represented by the operating system.
// For example, the path a\b is rejected on Windows, on which \ is a separator
// character and cannot be part of a filename.
//
// The path returned by Localize will always be local, as reported by IsLocal.
func Localize(path string) (string, error) {
return filepathlite.Localize(path)
}
// 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 {
return filepathlite.ToSlash(path)
}
// FromSlash returns the result of replacing each slash ('/') character
// in path with a separator character. Multiple slashes are replaced
// by multiple separators.
//
// See also the Localize function, which converts a slash-separated path
// as used by the io/fs package to an operating system path.
func FromSlash(path string) string {
return filepathlite.FromSlash(path)
}
// 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) {
return filepathlite.Split(path)
}
// 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 {
return filepathlite.Ext(path)
}
// 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)
}
// IsAbs reports whether the path is absolute.
func IsAbs(path string) bool {
return filepathlite.IsAbs(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 == "" && filepathlite.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 := bytealg.CountString(base[b0:bl], 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 [WalkFunc] 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 [WalkFunc] 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 := os.ReadDir(path)
if err != nil {
// Second call, to report ReadDir error.
err = walkDirFn(path, d, err)
if err != nil {
if err == SkipDir && d.IsDir() {
err = nil
}
return err
}
}
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, fs.FileInfoToDirEntry(info), fn)
}
if err == SkipDir || err == SkipAll {
return nil
}
return err
}
// 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
}
// 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
}
slices.Sort(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 {
return filepathlite.Base(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 {
return filepathlite.Dir(path)
}
// 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 filepathlite.VolumeName(path)
}

View File

@@ -0,0 +1,42 @@
// 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"
)
// 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
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
// 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"
)
// 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
}

View File

@@ -0,0 +1,114 @@
package filepath
import (
"os"
"strings"
"syscall"
)
// 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 os.IsPathSeparator(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 && os.IsPathSeparator(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 && strings.HasPrefix(e, "??") && (len(e) == len("??") || os.IsPathSeparator(e[2])) {
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())
}
func sameWord(a, b string) bool {
return strings.EqualFold(a, b)
}

View File

@@ -0,0 +1,709 @@
// 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/godebug"
"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 createMountPartition(t *testing.T, vhd string, args string) []byte {
testenv.MustHaveExecPath(t, "powershell")
t.Cleanup(func() {
cmd := testenv.Command(t, "powershell", "-Command", fmt.Sprintf("Dismount-VHD %q", vhd))
out, err := cmd.CombinedOutput()
if err != nil {
if t.Skipped() {
// Probably failed to dismount because we never mounted it in
// the first place. Log the error, but ignore it.
t.Logf("%v: %v (skipped)\n%s", cmd, err, out)
} else {
// Something went wrong, and we don't want to leave dangling VHDs.
// Better to fail the test than to just log the error and continue.
t.Errorf("%v: %v\n%s", cmd, err, out)
}
}
})
script := filepath.Join(t.TempDir(), "test.ps1")
cmd := strings.Join([]string{
"$ErrorActionPreference = \"Stop\"",
fmt.Sprintf("$vhd = New-VHD -Path %q -SizeBytes 3MB -Fixed", vhd),
"$vhd | Mount-VHD",
fmt.Sprintf("$vhd = Get-VHD %q", vhd),
"$vhd | Get-Disk | Initialize-Disk -PartitionStyle GPT",
"$part = $vhd | Get-Disk | New-Partition -UseMaximumSize -AssignDriveLetter:$false",
"$vol = $part | Format-Volume -FileSystem NTFS",
args,
}, "\n")
err := os.WriteFile(script, []byte(cmd), 0666)
if err != nil {
t.Fatal(err)
}
output, err := testenv.Command(t, "powershell", "-File", script).CombinedOutput()
if err != nil {
// This can happen if Hyper-V is not installed or enabled.
t.Skip("skipping test because failed to create VHD: ", err, string(output))
}
return output
}
var winsymlink = godebug.New("winsymlink")
var winreadlinkvolume = godebug.New("winreadlinkvolume")
func TestEvalSymlinksJunctionToVolumeID(t *testing.T) {
// Test that EvalSymlinks resolves a directory junction which
// is mapped to volumeID (instead of drive letter). See go.dev/issue/39786.
if winsymlink.Value() == "0" {
t.Skip("skipping test because winsymlink is not enabled")
}
t.Parallel()
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)
vhd := filepath.Join(tmpdir, "Test.vhdx")
output = createMountPartition(t, vhd, "Write-Host $vol.Path -NoNewline")
vol := string(output)
dirlink := filepath.Join(tmpdir, "dirlink")
output, err := testenv.Command(t, "cmd", "/c", "mklink", "/J", dirlink, vol).CombinedOutput()
if err != nil {
t.Fatalf("failed to run mklink %v %v: %v %q", dirlink, vol, err, output)
}
got, err := filepath.EvalSymlinks(dirlink)
if err != nil {
t.Fatal(err)
}
if got != dirlink {
t.Errorf(`EvalSymlinks(%q): got %q, want %q`, dirlink, got, dirlink)
}
}
func TestEvalSymlinksMountPointRecursion(t *testing.T) {
// Test that EvalSymlinks doesn't follow recursive mount points.
// See go.dev/issue/40176.
if winsymlink.Value() == "0" {
t.Skip("skipping test because winsymlink is not enabled")
}
t.Parallel()
tmpdir := tempDirCanonical(t)
dirlink := filepath.Join(tmpdir, "dirlink")
err := os.Mkdir(dirlink, 0755)
if err != nil {
t.Fatal(err)
}
vhd := filepath.Join(tmpdir, "Test.vhdx")
createMountPartition(t, vhd, fmt.Sprintf("$part | Add-PartitionAccessPath -AccessPath %q\n", dirlink))
got, err := filepath.EvalSymlinks(dirlink)
if err != nil {
t.Fatal(err)
}
if got != dirlink {
t.Errorf(`EvalSymlinks(%q): got %q, want %q`, dirlink, got, dirlink)
}
}
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)
}
var want string
if winsymlink.Value() == "0" {
if winreadlinkvolume.Value() == "0" {
want = vol + `\`
} else {
want = target
}
} else {
want = dirlink
}
if 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 = filepath.Join(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 winreadlinkvolume.Value() == "0" {
want = file
} else {
want = target
}
if 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)
}
}
}

View File

@@ -0,0 +1,150 @@
// 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"
"internal/filepathlite"
"io/fs"
"os"
"runtime"
"syscall"
)
func walkSymlinks(path string) (string, error) {
volLen := filepathlite.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[filepathlite.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) > filepathlite.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 := filepathlite.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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

230
src/path/match.go Normal file
View File

@@ -0,0 +1,230 @@
// 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 path
import (
"errors"
"internal/bytealg"
"unicode/utf8"
)
// ErrBadPattern indicates a pattern was malformed.
var ErrBadPattern = errors.New("syntax error in pattern")
// Match reports whether name matches the shell pattern.
// The pattern syntax is:
//
// pattern:
// { term }
// term:
// '*' matches any sequence of non-/ characters
// '?' matches any single non-/ 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.
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 bytealg.IndexByteString(name, '/') < 0, 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] != '/'; 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
}
}
}
// Before returning false with no error,
// check that the remainder of the pattern is syntactically valid.
for len(pattern) > 0 {
_, chunk, pattern = scanChunk(pattern)
if _, _, err := matchChunk(chunk, ""); 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 '\\':
// 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] == '/' {
failed = true
}
_, n := utf8.DecodeRuneInString(s)
s = s[n:]
}
chunk = chunk[1:]
case '\\':
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] == '\\' {
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
}

84
src/path/match_test.go Normal file
View File

@@ -0,0 +1,84 @@
// 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 path_test
import (
. "path"
"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 TestMatch(t *testing.T) {
for _, tt := range matchTests {
ok, err := Match(tt.pattern, tt.s)
if ok != tt.match || err != tt.err {
t.Errorf("Match(%#q, %#q) = %v, %v want %v, %v", tt.pattern, tt.s, ok, err, tt.match, tt.err)
}
}
}

226
src/path/path.go Normal file
View File

@@ -0,0 +1,226 @@
// 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 path implements utility routines for manipulating slash-separated
// paths.
//
// The path package should only be used for paths separated by forward
// slashes, such as the paths in URLs. This package does not deal with
// Windows paths with drive letters or backslashes; to manipulate
// operating system paths, use the [path/filepath] package.
package path
import "internal/bytealg"
// 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 {
s string
buf []byte
w int
}
func (b *lazybuf) index(i int) byte {
if b.buf != nil {
return b.buf[i]
}
return b.s[i]
}
func (b *lazybuf) append(c byte) {
if b.buf == nil {
if b.w < len(b.s) && b.s[b.w] == c {
b.w++
return
}
b.buf = make([]byte, len(b.s))
copy(b.buf, b.s[:b.w])
}
b.buf[b.w] = c
b.w++
}
func (b *lazybuf) string() string {
if b.buf == nil {
return b.s[:b.w]
}
return string(b.buf[:b.w])
}
// 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 slashes with a single slash.
// 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.
//
// The returned path ends in a slash only if it is the root "/".
//
// If the result of this process is an empty string, Clean
// returns the string ".".
//
// See also Rob Pike, “Lexical File Names in Plan 9 or
// Getting Dot-Dot Right,”
// https://9p.io/sys/doc/lexnames.html
func Clean(path string) string {
if path == "" {
return "."
}
rooted := path[0] == '/'
n := len(path)
// 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.
out := lazybuf{s: path}
r, dotdot := 0, 0
if rooted {
out.append('/')
r, dotdot = 1, 1
}
for r < n {
switch {
case path[r] == '/':
// empty path element
r++
case path[r] == '.' && (r+1 == n || path[r+1] == '/'):
// . element
r++
case path[r] == '.' && path[r+1] == '.' && (r+2 == n || path[r+2] == '/'):
// .. element: remove to last /
r += 2
switch {
case out.w > dotdot:
// can backtrack
out.w--
for out.w > dotdot && out.index(out.w) != '/' {
out.w--
}
case !rooted:
// cannot backtrack, but not rooted, so append .. element.
if out.w > 0 {
out.append('/')
}
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('/')
}
// copy element
for ; r < n && path[r] != '/'; r++ {
out.append(path[r])
}
}
}
// Turn empty string into "."
if out.w == 0 {
return "."
}
return out.string()
}
// Split splits path immediately following the final slash,
// separating it into a directory and file name component.
// If there is no slash 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) {
i := bytealg.LastIndexByteString(path, '/')
return path[:i+1], path[i+1:]
}
// Join joins any number of path elements into a single path,
// separating them with slashes. 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.
func Join(elem ...string) string {
size := 0
for _, e := range elem {
size += len(e)
}
if size == 0 {
return ""
}
buf := make([]byte, 0, size+len(elem)-1)
for _, e := range elem {
if len(buf) > 0 || e != "" {
if len(buf) > 0 {
buf = append(buf, '/')
}
buf = append(buf, e...)
}
}
return Clean(string(buf))
}
// Ext returns the file name extension used by path.
// The extension is the suffix beginning at the final dot
// in the final slash-separated element of path;
// it is empty if there is no dot.
func Ext(path string) string {
for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {
if path[i] == '.' {
return path[i:]
}
}
return ""
}
// Base returns the last element of path.
// Trailing slashes are removed before extracting the last element.
// If the path is empty, Base returns ".".
// If the path consists entirely of slashes, Base returns "/".
func Base(path string) string {
if path == "" {
return "."
}
// Strip trailing slashes.
for len(path) > 0 && path[len(path)-1] == '/' {
path = path[0 : len(path)-1]
}
// Find the last element
if i := bytealg.LastIndexByteString(path, '/'); i >= 0 {
path = path[i+1:]
}
// If empty now, it had only slashes.
if path == "" {
return "/"
}
return path
}
// IsAbs reports whether the path is absolute.
func IsAbs(path string) bool {
return len(path) > 0 && path[0] == '/'
}
// Dir returns all but the last element of path, typically the path's directory.
// After dropping the final element using [Split], the path is Cleaned and trailing
// slashes are removed.
// If the path is empty, Dir returns ".".
// If the path consists entirely of slashes followed by non-slash bytes, Dir
// returns a single slash. In any other case, the returned path does not end in a
// slash.
func Dir(path string) string {
dir, _ := Split(path)
return Clean(dir)
}

236
src/path/path_test.go Normal file
View File

@@ -0,0 +1,236 @@
// 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 path_test
import (
. "path"
"runtime"
"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"},
{"/", "/"},
// 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"},
{"///abc", "/abc"},
{"//abc//", "/abc"},
{"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"},
// Combinations
{"abc/./../def", "def"},
{"abc//./../def", "def"},
{"abc/../../././../def", "../../def"},
}
func TestClean(t *testing.T) {
for _, test := range cleantests {
if s := Clean(test.path); s != test.result {
t.Errorf("Clean(%q) = %q, want %q", test.path, s, test.result)
}
if s := Clean(test.result); s != test.result {
t.Errorf("Clean(%q) = %q, want %q", test.result, s, test.result)
}
}
}
func TestCleanMallocs(t *testing.T) {
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 cleantests {
allocs := testing.AllocsPerRun(100, func() { Clean(test.result) })
if allocs > 0 {
t.Errorf("Clean(%q): %v allocs, want zero", test.result, allocs)
}
}
}
type SplitTest struct {
path, dir, file string
}
var splittests = []SplitTest{
{"a/b", "a/", "b"},
{"a/b/", "a/b/", ""},
{"a/", "a/", ""},
{"a", "", "a"},
{"/", "/", ""},
}
func TestSplit(t *testing.T) {
for _, test := range splittests {
if d, f := 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{"a"}, "a"},
// two parameters
{[]string{"a", "b"}, "a/b"},
{[]string{"a", ""}, "a"},
{[]string{"", "b"}, "b"},
{[]string{"/", "a"}, "/a"},
{[]string{"/", ""}, "/"},
{[]string{"a/", "b"}, "a/b"},
{[]string{"a/", ""}, "a"},
{[]string{"", ""}, ""},
}
func TestJoin(t *testing.T) {
for _, test := range jointests {
if p := Join(test.elem...); p != test.path {
t.Errorf("Join(%q) = %q, want %q", test.elem, p, test.path)
}
}
}
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 := Ext(test.path); x != test.ext {
t.Errorf("Ext(%q) = %q, want %q", test.path, x, test.ext)
}
}
}
var basetests = []PathTest{
// Already clean
{"", "."},
{".", "."},
{"/.", "."},
{"/", "/"},
{"////", "/"},
{"x/", "x"},
{"abc", "abc"},
{"abc/def", "def"},
{"a/b/.x", ".x"},
{"a/b/c.", "c."},
{"a/b/c.x", "c.x"},
}
func TestBase(t *testing.T) {
for _, test := range basetests {
if s := 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"},
{"abc////def", "abc"},
{"a/b/.x", "a/b"},
{"a/b/c.", "a/b"},
{"a/b/c.x", "a/b"},
}
func TestDir(t *testing.T) {
for _, test := range dirtests {
if s := 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},
}
func TestIsAbs(t *testing.T) {
for _, test := range isAbsTests {
if r := IsAbs(test.path); r != test.isAbs {
t.Errorf("IsAbs(%q) = %v, want %v", test.path, r, test.isAbs)
}
}
}