Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit bae699a

Browse files
committedApr 26, 2017
Replace uses of filepath.HasPrefix with a path-aware function
internal.HasFilepathPrefix determines whether a given path is contained in another, being careful to take into account that "/foo" doesn't contain "/foobar" and that there are case-sensitive and case-insensitive fileystems out there. This fixes issue golang#296. Signed-off-by: Marcelo E. Magallon <[email protected]>
1 parent a28d05c commit bae699a

File tree

5 files changed

+261
-18
lines changed

5 files changed

+261
-18
lines changed
 

‎.travis.yml

+1-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ before_script:
2121
script:
2222
- go build -v ./cmd/dep
2323
- go vet $PKGS
24-
# Ignore the deprecation warning about filepath.HasPrefix (SA1019). This flag
25-
# can be removed when issue #296 is resolved.
26-
- staticcheck -ignore='github.com/golang/dep/context.go:SA1019 github.com/golang/dep/cmd/dep/init.go:SA1019' $PKGS
24+
- staticcheck $PKGS
2725
- gosimple $PKGS
2826
- test -z "$(gofmt -s -l . 2>&1 | grep -v vendor/ | tee /dev/stderr)"
2927
- go test -race $PKGS

‎context.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212

1313
"github.com/Masterminds/vcs"
14+
"github.com/golang/dep/internal"
1415
"github.com/pkg/errors"
1516
"github.com/sdboyer/gps"
1617
)
@@ -37,7 +38,7 @@ func NewContext() (*Ctx, error) {
3738
for _, gp := range filepath.SplitList(buildContext.GOPATH) {
3839
gp = filepath.FromSlash(gp)
3940

40-
if filepath.HasPrefix(wd, gp) {
41+
if internal.HasFilepathPrefix(wd, gp) {
4142
ctx.GOPATH = gp
4243
}
4344

@@ -164,7 +165,7 @@ func (c *Ctx) resolveProjectRoot(path string) (string, error) {
164165
// Determine if the symlink is within any of the GOPATHs, in which case we're not
165166
// sure how to resolve it.
166167
for _, gp := range c.GOPATHS {
167-
if filepath.HasPrefix(path, gp) {
168+
if internal.HasFilepathPrefix(path, gp) {
168169
return "", errors.Errorf("'%s' is linked to another path within a GOPATH (%s)", path, gp)
169170
}
170171
}
@@ -179,7 +180,7 @@ func (c *Ctx) resolveProjectRoot(path string) (string, error) {
179180
// The second returned string indicates which GOPATH value was used.
180181
func (c *Ctx) SplitAbsoluteProjectRoot(path string) (string, error) {
181182
srcprefix := filepath.Join(c.GOPATH, "src") + string(filepath.Separator)
182-
if filepath.HasPrefix(path, srcprefix) {
183+
if internal.HasFilepathPrefix(path, srcprefix) {
183184
// filepath.ToSlash because we're dealing with an import path now,
184185
// not an fs path
185186
return filepath.ToSlash(path[len(srcprefix):]), nil

‎fs.go

+2-12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"runtime"
1313
"syscall"
1414

15+
"github.com/golang/dep/internal"
1516
"github.com/pelletier/go-toml"
1617
"github.com/pkg/errors"
1718
)
@@ -32,18 +33,7 @@ func IsRegular(name string) (bool, error) {
3233
}
3334

3435
func IsDir(name string) (bool, error) {
35-
// TODO: lstat?
36-
fi, err := os.Stat(name)
37-
if os.IsNotExist(err) {
38-
return false, nil
39-
}
40-
if err != nil {
41-
return false, err
42-
}
43-
if !fi.IsDir() {
44-
return false, errors.Errorf("%q is not a directory", name)
45-
}
46-
return true, nil
36+
return internal.IsDir(name)
4737
}
4838

4939
func IsNonEmptyDir(name string) (bool, error) {

‎internal/fs.go

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright 2016 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package internal
6+
7+
import (
8+
"os"
9+
"path/filepath"
10+
"strings"
11+
"unicode"
12+
13+
"github.com/pkg/errors"
14+
)
15+
16+
func IsDir(name string) (bool, error) {
17+
// TODO: lstat?
18+
fi, err := os.Stat(name)
19+
if os.IsNotExist(err) {
20+
return false, nil
21+
}
22+
if err != nil {
23+
return false, err
24+
}
25+
if !fi.IsDir() {
26+
return false, errors.Errorf("%q is not a directory", name)
27+
}
28+
return true, nil
29+
}
30+
31+
// HasFilepathPrefix will determine if "path" starts with "prefix" from
32+
// the point of view of a filesystem.
33+
//
34+
// Unlike filepath.HasPrefix, this function is path-aware, meaning that
35+
// it knows that two directories /foo and /foobar are not the same
36+
// thing, and therefore HasFilepathPrefix("/foobar", "/foo") will return
37+
// false.
38+
//
39+
// This function also handles the case where the involved filesystems
40+
// are case-insensitive, meaning /foo/bar and /Foo/Bar correspond to the
41+
// same file. In that situation HasFilepathPrefix("/Foo/Bar", "/foo")
42+
// will return true. The implementation is *not* OS-specific, so a FAT32
43+
// filesystem mounted on Linux will be handled correctly.
44+
func HasFilepathPrefix(path, prefix string) bool {
45+
if filepath.VolumeName(path) != filepath.VolumeName(prefix) {
46+
return false
47+
}
48+
49+
var dn string
50+
51+
if isDir, err := IsDir(path); err != nil {
52+
return false
53+
} else if isDir {
54+
dn = path
55+
} else {
56+
dn = filepath.Dir(path)
57+
}
58+
59+
dn = strings.TrimSuffix(dn, string(os.PathSeparator))
60+
prefix = strings.TrimSuffix(prefix, string(os.PathSeparator))
61+
62+
dirs := strings.Split(dn, string(os.PathSeparator))[1:]
63+
prefixes := strings.Split(prefix, string(os.PathSeparator))[1:]
64+
65+
if len(prefixes) > len(dirs) {
66+
return false
67+
}
68+
69+
var d, p string
70+
71+
for i := range prefixes {
72+
// need to test each component of the path for
73+
// case-sensitiveness because on Unix we could have
74+
// something like ext4 filesystem mounted on FAT
75+
// mountpoint, mounted on ext4 filesystem, i.e. the
76+
// problematic filesystem is not the last one.
77+
if isCaseSensitiveFilesystem(filepath.Join(d, dirs[i])) {
78+
d = filepath.Join(d, dirs[i])
79+
p = filepath.Join(p, prefixes[i])
80+
} else {
81+
d = filepath.Join(d, strings.ToLower(dirs[i]))
82+
p = filepath.Join(p, strings.ToLower(prefixes[i]))
83+
}
84+
85+
if p != d {
86+
return false
87+
}
88+
}
89+
90+
return true
91+
}
92+
93+
// genTestFilename returns a string with at most one rune case-flipped.
94+
//
95+
// The transformation is applied only to the first rune that can be
96+
// reversibly case-flipped, meaning:
97+
//
98+
// * A lowercase rune for which it's true that lower(upper(r)) == r
99+
// * An uppercase rune for which it's true that upper(lower(r)) == r
100+
//
101+
// All the other runes are left intact.
102+
func genTestFilename(str string) string {
103+
flip := true
104+
return strings.Map(func(r rune) rune {
105+
if flip {
106+
if unicode.IsLower(r) {
107+
u := unicode.ToUpper(r)
108+
if unicode.ToLower(u) == r {
109+
r = u
110+
flip = false
111+
}
112+
} else if unicode.IsUpper(r) {
113+
l := unicode.ToLower(r)
114+
if unicode.ToUpper(l) == r {
115+
r = l
116+
flip = false
117+
}
118+
}
119+
}
120+
return r
121+
}, str)
122+
}
123+
124+
// isCaseSensitiveFilesystem determines if the filesystem where dir
125+
// exists is case sensitive or not.
126+
//
127+
// CAVEAT: this function works by taking the last component of the given
128+
// path and flipping the case of the first letter for which case
129+
// flipping is a reversible operation (/foo/Bar → /foo/bar), then
130+
// testing for the existence of the new filename. There are two
131+
// possibilities:
132+
//
133+
// 1. The alternate filename does not exist. We can conclude that the
134+
// filesystem is case sensitive.
135+
//
136+
// 2. The filename happens to exist. We have to test if the two files
137+
// are the same file (case insensitive file system) or different ones
138+
// (case sensitive filesystem).
139+
//
140+
// If the input directory is such that the last component is composed
141+
// exclusively of case-less codepoints (e.g. numbers), this function will
142+
// return false.
143+
func isCaseSensitiveFilesystem(dir string) bool {
144+
alt := filepath.Join(filepath.Dir(dir),
145+
genTestFilename(filepath.Base(dir)))
146+
147+
dInfo, err := os.Stat(dir)
148+
if err != nil {
149+
return true
150+
}
151+
152+
aInfo, err := os.Stat(alt)
153+
if err != nil {
154+
return true
155+
}
156+
157+
return !os.SameFile(dInfo, aInfo)
158+
}

‎internal/fs_test.go

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2016 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package internal
6+
7+
import (
8+
"io/ioutil"
9+
"os"
10+
"path/filepath"
11+
"testing"
12+
)
13+
14+
func TestHasFilepathPrefix(t *testing.T) {
15+
dir, err := ioutil.TempDir("", "dep")
16+
if err != nil {
17+
t.Fatal(err)
18+
}
19+
defer os.RemoveAll(dir)
20+
21+
cases := []struct {
22+
dir string
23+
prefix string
24+
want bool
25+
}{
26+
{filepath.Join(dir, "a", "b"), filepath.Join(dir), true},
27+
{filepath.Join(dir, "a", "b"), filepath.Join(dir, "a"), true},
28+
{filepath.Join(dir, "a", "b"), filepath.Join(dir, "a", "b"), true},
29+
{filepath.Join(dir, "a", "b"), filepath.Join(dir, "c"), false},
30+
{filepath.Join(dir, "a", "b"), filepath.Join(dir, "a", "d", "b"), false},
31+
{filepath.Join(dir, "a", "b"), filepath.Join(dir, "a", "b2"), false},
32+
{filepath.Join(dir, "ab"), filepath.Join(dir, "a", "b"), false},
33+
{filepath.Join(dir, "ab"), filepath.Join(dir, "a"), false},
34+
{filepath.Join(dir, "123"), filepath.Join(dir, "123"), true},
35+
{filepath.Join(dir, "123"), filepath.Join(dir, "1"), false},
36+
{filepath.Join(dir, "⌘"), filepath.Join(dir, "⌘"), true},
37+
{filepath.Join(dir, "a"), filepath.Join(dir, "⌘"), false},
38+
{filepath.Join(dir, "⌘"), filepath.Join(dir, "a"), false},
39+
}
40+
41+
for _, c := range cases {
42+
err := os.MkdirAll(c.dir, 0755)
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
47+
err = os.MkdirAll(c.prefix, 0755)
48+
if err != nil {
49+
t.Fatal(err)
50+
}
51+
52+
got := HasFilepathPrefix(c.dir, c.prefix)
53+
if c.want != got {
54+
t.Fatalf("dir: %q, prefix: %q, expected: %v, got: %v", c.dir, c.prefix, c.want, got)
55+
}
56+
}
57+
}
58+
59+
func TestGenTestFilename(t *testing.T) {
60+
cases := []struct {
61+
str string
62+
want string
63+
}{
64+
{"abc", "Abc"},
65+
{"ABC", "aBC"},
66+
{"AbC", "abC"},
67+
{"αβγ", "Αβγ"},
68+
{"123", "123"},
69+
{"1a2", "1A2"},
70+
{"12a", "12A"},
71+
{"⌘", "⌘"},
72+
}
73+
74+
for _, c := range cases {
75+
got := genTestFilename(c.str)
76+
if c.want != got {
77+
t.Fatalf("str: %q, expected: %q, got: %q", c.str, c.want, got)
78+
}
79+
}
80+
}
81+
82+
func BenchmarkGenTestFilename(b *testing.B) {
83+
cases := []string{
84+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
85+
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
86+
"αααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααα",
87+
"11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",
88+
"⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘",
89+
}
90+
91+
for i := 0; i < b.N; i++ {
92+
for _, str := range cases {
93+
genTestFilename(str)
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)
Please sign in to comment.