diff options
Diffstat (limited to 'src/pkg/path')
-rw-r--r-- | src/pkg/path/filepath/example_unix_test.go | 39 | ||||
-rw-r--r-- | src/pkg/path/filepath/match_test.go | 3 | ||||
-rw-r--r-- | src/pkg/path/filepath/path.go | 130 | ||||
-rw-r--r-- | src/pkg/path/filepath/path_plan9.go | 15 | ||||
-rw-r--r-- | src/pkg/path/filepath/path_test.go | 52 | ||||
-rw-r--r-- | src/pkg/path/filepath/path_unix.go | 15 | ||||
-rw-r--r-- | src/pkg/path/filepath/path_windows.go | 55 | ||||
-rw-r--r-- | src/pkg/path/filepath/path_windows_test.go | 89 | ||||
-rw-r--r-- | src/pkg/path/filepath/symlink_windows.go | 10 | ||||
-rw-r--r-- | src/pkg/path/path.go | 83 | ||||
-rw-r--r-- | src/pkg/path/path_test.go | 11 |
11 files changed, 407 insertions, 95 deletions
diff --git a/src/pkg/path/filepath/example_unix_test.go b/src/pkg/path/filepath/example_unix_test.go new file mode 100644 index 000000000..f3fe076c3 --- /dev/null +++ b/src/pkg/path/filepath/example_unix_test.go @@ -0,0 +1,39 @@ +// 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. + +// +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 +} diff --git a/src/pkg/path/filepath/match_test.go b/src/pkg/path/filepath/match_test.go index 7b0ea8017..f1bc60e35 100644 --- a/src/pkg/path/filepath/match_test.go +++ b/src/pkg/path/filepath/match_test.go @@ -2,10 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package filepath_test +package filepath import ( - . "path/filepath" "runtime" "strings" "testing" diff --git a/src/pkg/path/filepath/path.go b/src/pkg/path/filepath/path.go index 815021bd0..f8c7e4b2f 100644 --- a/src/pkg/path/filepath/path.go +++ b/src/pkg/path/filepath/path.go @@ -13,6 +13,45 @@ import ( "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) 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 @@ -40,14 +79,15 @@ const ( // Getting Dot-Dot Right,'' // http://plan9.bell-labs.com/sys/doc/lexnames.html func Clean(path string) string { - vol := VolumeName(path) - path = path[len(vol):] + originalPath := path + volLen := volumeNameLen(path) + path = path[volLen:] if path == "" { - if len(vol) > 1 && vol[1] != ':' { + if volLen > 1 && originalPath[1] != ':' { // should be UNC - return FromSlash(vol) + return FromSlash(originalPath) } - return vol + "." + return originalPath + "." } rooted := os.IsPathSeparator(path[0]) @@ -57,11 +97,11 @@ func Clean(path string) string { // dotdot is index in buf where .. must stop, either because // it is the leading slash or it is a leading ../../.. prefix. n := len(path) - buf := []byte(path) - r, w, dotdot := 0, 0, 0 + out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen} + r, dotdot := 0, 0 if rooted { - buf[0] = Separator - r, w, dotdot = 1, 1, 1 + out.append(Separator) + r, dotdot = 1, 1 } for r < n { @@ -76,46 +116,40 @@ func Clean(path string) string { // .. element: remove to last separator r += 2 switch { - case w > dotdot: + case out.w > dotdot: // can backtrack - w-- - for w > dotdot && !os.IsPathSeparator(buf[w]) { - w-- + 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 w > 0 { - buf[w] = Separator - w++ + if out.w > 0 { + out.append(Separator) } - buf[w] = '.' - w++ - buf[w] = '.' - w++ - dotdot = w + out.append('.') + out.append('.') + dotdot = out.w } default: // real path element. // add slash if needed - if rooted && w != 1 || !rooted && w != 0 { - buf[w] = Separator - w++ + if rooted && out.w != 1 || !rooted && out.w != 0 { + out.append(Separator) } // copy element for ; r < n && !os.IsPathSeparator(path[r]); r++ { - buf[w] = path[r] - w++ + out.append(path[r]) } } } // Turn empty string into "." - if w == 0 { - buf[w] = '.' - w++ + if out.w == 0 { + out.append('.') } - return FromSlash(vol + string(buf[0:w])) + return FromSlash(out.string()) } // ToSlash returns the result of replacing each separator character @@ -142,10 +176,7 @@ func FromSlash(path string) string { // 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 { - if path == "" { - return []string{} - } - return strings.Split(path, string(ListSeparator)) + return splitList(path) } // Split splits path immediately following the final Separator, @@ -291,13 +322,18 @@ func Rel(basepath, targpath string) (string, error) { var SkipDir = errors.New("skip this directory") // WalkFunc is the type of the function called for each file or directory -// visited by Walk. If there was a problem walking to the file or directory -// named by path, the incoming error will describe the problem and the -// function can decide how to handle that error (and Walk will not descend -// into that directory). If an error is returned, processing stops. The -// sole exception is that if path is a directory and the function returns the -// special value SkipDir, the contents of the directory are skipped -// and processing continues as usual on the next file. +// visited by Walk. The path argument contains the argument to Walk as a +// prefix; that is, if Walk is called with "dir", which is a directory +// containing the file "a", the walk function will be called with argument +// "dir/a". The info argument is the os.FileInfo for the named path. +// +// If there was a problem walking to the file or directory named by path, the +// incoming error will describe the problem and the function can decide how +// to handle that error (and Walk will not descend into that directory). If +// an error is returned, processing stops. The sole exception is that if path +// is a directory and the function returns the special value SkipDir, the +// contents of the directory are skipped and processing continues as usual on +// the next file. type WalkFunc func(path string, info os.FileInfo, err error) error // walk recursively descends path, calling w. @@ -335,6 +371,7 @@ func walk(path string, info os.FileInfo, walkFn WalkFunc) error { // and directories are filtered by walkFn. The files are walked in lexical // order, which makes the output deterministic but means that for very // large directories Walk can be inefficient. +// Walk does not follow symbolic links. func Walk(root string, walkFn WalkFunc) error { info, err := os.Lstat(root) if err != nil { @@ -397,7 +434,8 @@ func Base(path string) string { } // Dir returns all but the last element of path, typically the path's directory. -// Trailing path separators are removed before processing. +// After dropping the final element, the path is Cleaned 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. @@ -417,3 +455,11 @@ func Dir(path string) string { } return vol + dir } + +// VolumeName returns leading volume name. +// Given "C:\foo\bar" it returns "C:" under windows. +// Given "\\host\share\foo" it returns "\\host\share". +// On other platforms it returns "". +func VolumeName(path string) (v string) { + return path[:volumeNameLen(path)] +} diff --git a/src/pkg/path/filepath/path_plan9.go b/src/pkg/path/filepath/path_plan9.go index 59a5812dd..12e85aae0 100644 --- a/src/pkg/path/filepath/path_plan9.go +++ b/src/pkg/path/filepath/path_plan9.go @@ -11,13 +11,20 @@ func IsAbs(path string) bool { return strings.HasPrefix(path, "/") || strings.HasPrefix(path, "#") } -// VolumeName returns the leading volume name on Windows. -// It returns "" elsewhere. -func VolumeName(path string) string { - return "" +// 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. 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)) +} diff --git a/src/pkg/path/filepath/path_test.go b/src/pkg/path/filepath/path_test.go index 070905fd3..e768ad32f 100644 --- a/src/pkg/path/filepath/path_test.go +++ b/src/pkg/path/filepath/path_test.go @@ -20,7 +20,6 @@ type PathTest struct { var cleantests = []PathTest{ // Already clean - {"", "."}, {"abc", "abc"}, {"abc/def", "abc/def"}, {"a/b/c", "a/b/c"}, @@ -31,6 +30,9 @@ var cleantests = []PathTest{ {"/abc", "/abc"}, {"/", "/"}, + // Empty is current dir + {"", "."}, + // Remove trailing slash {"abc/", "abc"}, {"abc/def/", "abc/def"}, @@ -61,6 +63,7 @@ var cleantests = []PathTest{ {"abc/def/../../..", ".."}, {"/abc/def/../../..", "/"}, {"abc/def/../../../ghi/jkl/../../../mno", "../../mno"}, + {"/../abc", "/abc"}, // Combinations {"abc/./../def", "def"}, @@ -99,6 +102,16 @@ func TestClean(t *testing.T) { 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) + } + } + + 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) + } } } @@ -135,10 +148,36 @@ var splitlisttests = []SplitListTest{ {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) { - for _, test := range splitlisttests { + 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) = %s, want %s", test.list, l, test.result) + t.Errorf("SplitList(%#q) = %#q, want %#q", test.list, l, test.result) } } } @@ -684,10 +723,15 @@ func TestAbs(t *testing.T) { } defer os.RemoveAll(root) + 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) @@ -871,7 +915,7 @@ func TestDriveLetterInEvalSymlinks(t *testing.T) { } func TestBug3486(t *testing.T) { // http://code.google.com/p/go/issues/detail?id=3486 - root, err := filepath.EvalSymlinks(os.Getenv("GOROOT")) + root, err := filepath.EvalSymlinks(runtime.GOROOT()) if err != nil { t.Fatal(err) } diff --git a/src/pkg/path/filepath/path_unix.go b/src/pkg/path/filepath/path_unix.go index 305e30727..cff7b2c65 100644 --- a/src/pkg/path/filepath/path_unix.go +++ b/src/pkg/path/filepath/path_unix.go @@ -13,13 +13,20 @@ func IsAbs(path string) bool { return strings.HasPrefix(path, "/") } -// VolumeName returns the leading volume name on Windows. -// It returns "" elsewhere. -func VolumeName(path string) string { - return "" +// 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. 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)) +} diff --git a/src/pkg/path/filepath/path_windows.go b/src/pkg/path/filepath/path_windows.go index 3dcd03021..e99997257 100644 --- a/src/pkg/path/filepath/path_windows.go +++ b/src/pkg/path/filepath/path_windows.go @@ -14,29 +14,27 @@ func isSlash(c uint8) bool { // IsAbs returns true if the path is absolute. func IsAbs(path string) (b bool) { - v := VolumeName(path) - if v == "" { + l := volumeNameLen(path) + if l == 0 { return false } - path = path[len(v):] + path = path[l:] if path == "" { return false } return isSlash(path[0]) } -// VolumeName returns leading volume name. -// Given "C:\foo\bar" it returns "C:" under windows. -// Given "\\host\share\foo" it returns "\\host\share". -// On other platforms it returns "". -func VolumeName(path string) (v string) { +// volumeNameLen returns length of the leading volume name on Windows. +// It returns 0 elsewhere. +func volumeNameLen(path string) int { if len(path) < 2 { - return "" + return 0 } // with drive letter c := path[0] if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') { - return path[:2] + return 2 } // is it UNC if l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) && @@ -56,13 +54,13 @@ func VolumeName(path string) (v string) { break } } - return path[:n] + return n } break } } } - return "" + return 0 } // HasPrefix exists for historical compatibility and should not be used. @@ -72,3 +70,36 @@ func HasPrefix(p, prefix string) bool { } 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 { + if strings.Contains(s, `"`) { + list[i] = strings.Replace(s, `"`, ``, -1) + } + } + + return list +} diff --git a/src/pkg/path/filepath/path_windows_test.go b/src/pkg/path/filepath/path_windows_test.go new file mode 100644 index 000000000..d8926adde --- /dev/null +++ b/src/pkg/path/filepath/path_windows_test.go @@ -0,0 +1,89 @@ +package filepath_test + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "reflect" + "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 os.FileMode = 0700 + ) + + tmp, err := ioutil.TempDir("", "testWinSplitListTestIsValid") + if err != nil { + t.Fatalf("TempDir failed: %v", err) + } + defer os.RemoveAll(tmp) + + 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 = ioutil.WriteFile(fn, data, perm); err != nil { + t.Errorf("%d,%d: WriteFile(%#q) failed: %v", ti, i, fn, err) + return + } + } + + 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=` + tt.list}, + 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) + } + } + } +} diff --git a/src/pkg/path/filepath/symlink_windows.go b/src/pkg/path/filepath/symlink_windows.go index 1ee939928..9adc8a48a 100644 --- a/src/pkg/path/filepath/symlink_windows.go +++ b/src/pkg/path/filepath/symlink_windows.go @@ -9,7 +9,10 @@ import ( ) func toShort(path string) (string, error) { - p := syscall.StringToUTF16(path) + p, err := syscall.UTF16FromString(path) + if err != nil { + return "", err + } b := p // GetShortPathName says we can reuse buffer n, err := syscall.GetShortPathName(&p[0], &b[0], uint32(len(b))) if err != nil { @@ -26,7 +29,10 @@ func toShort(path string) (string, error) { } func toLong(path string) (string, error) { - p := syscall.StringToUTF16(path) + p, err := syscall.UTF16FromString(path) + if err != nil { + return "", err + } b := p // GetLongPathName says we can reuse buffer n, err := syscall.GetLongPathName(&p[0], &b[0], uint32(len(b))) if err != nil { diff --git a/src/pkg/path/path.go b/src/pkg/path/path.go index a7e041568..bdb85c6b9 100644 --- a/src/pkg/path/path.go +++ b/src/pkg/path/path.go @@ -10,6 +10,43 @@ import ( "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 { + 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: @@ -42,10 +79,11 @@ func Clean(path string) string { // 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. - buf := []byte(path) - r, w, dotdot := 0, 0, 0 + out := lazybuf{s: path} + r, dotdot := 0, 0 if rooted { - r, w, dotdot = 1, 1, 1 + out.append('/') + r, dotdot = 1, 1 } for r < n { @@ -60,46 +98,40 @@ func Clean(path string) string { // .. element: remove to last / r += 2 switch { - case w > dotdot: + case out.w > dotdot: // can backtrack - w-- - for w > dotdot && buf[w] != '/' { - w-- + out.w-- + for out.w > dotdot && out.index(out.w) != '/' { + out.w-- } case !rooted: // cannot backtrack, but not rooted, so append .. element. - if w > 0 { - buf[w] = '/' - w++ + if out.w > 0 { + out.append('/') } - buf[w] = '.' - w++ - buf[w] = '.' - w++ - dotdot = w + out.append('.') + out.append('.') + dotdot = out.w } default: // real path element. // add slash if needed - if rooted && w != 1 || !rooted && w != 0 { - buf[w] = '/' - w++ + if rooted && out.w != 1 || !rooted && out.w != 0 { + out.append('/') } // copy element for ; r < n && path[r] != '/'; r++ { - buf[w] = path[r] - w++ + out.append(path[r]) } } } // Turn empty string into "." - if w == 0 { - buf[w] = '.' - w++ + if out.w == 0 { + return "." } - return string(buf[0:w]) + return out.string() } // Split splits path immediately following the final slash. @@ -166,7 +198,8 @@ func IsAbs(path string) bool { } // Dir returns all but the last element of path, typically the path's directory. -// The path is Cleaned and trailing slashes are removed before processing. +// 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 diff --git a/src/pkg/path/path_test.go b/src/pkg/path/path_test.go index 77f080433..220ec1a0b 100644 --- a/src/pkg/path/path_test.go +++ b/src/pkg/path/path_test.go @@ -67,6 +67,16 @@ func TestClean(t *testing.T) { 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) + } + } + + 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) + } } } @@ -181,6 +191,7 @@ var dirtests = []PathTest{ {"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"}, |