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 #296.

Signed-off-by: Marcelo E. Magallon <marcelo.magallon@gmail.com>
This commit is contained in:
Marcelo E. Magallon 2017-04-16 21:20:48 -06:00
Родитель a28d05c680
Коммит c409fbd7e3
6 изменённых файлов: 262 добавлений и 19 удалений

Просмотреть файл

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

Просмотреть файл

@ -44,7 +44,7 @@ func (cmd *initCommand) Register(fs *flag.FlagSet) {}
type initCommand struct{}
func trimPathPrefix(p1, p2 string) string {
if filepath.HasPrefix(p1, p2) {
if internal.HasFilepathPrefix(p1, p2) {
return p1[len(p2):]
}
return p1

Просмотреть файл

@ -11,6 +11,7 @@ import (
"strings"
"github.com/Masterminds/vcs"
"github.com/golang/dep/internal"
"github.com/pkg/errors"
"github.com/sdboyer/gps"
)
@ -37,7 +38,7 @@ func NewContext() (*Ctx, error) {
for _, gp := range filepath.SplitList(buildContext.GOPATH) {
gp = filepath.FromSlash(gp)
if filepath.HasPrefix(wd, gp) {
if internal.HasFilepathPrefix(wd, gp) {
ctx.GOPATH = gp
}
@ -164,7 +165,7 @@ func (c *Ctx) resolveProjectRoot(path string) (string, error) {
// Determine if the symlink is within any of the GOPATHs, in which case we're not
// sure how to resolve it.
for _, gp := range c.GOPATHS {
if filepath.HasPrefix(path, gp) {
if internal.HasFilepathPrefix(path, gp) {
return "", errors.Errorf("'%s' is linked to another path within a GOPATH (%s)", path, gp)
}
}
@ -179,7 +180,7 @@ func (c *Ctx) resolveProjectRoot(path string) (string, error) {
// The second returned string indicates which GOPATH value was used.
func (c *Ctx) SplitAbsoluteProjectRoot(path string) (string, error) {
srcprefix := filepath.Join(c.GOPATH, "src") + string(filepath.Separator)
if filepath.HasPrefix(path, srcprefix) {
if internal.HasFilepathPrefix(path, srcprefix) {
// filepath.ToSlash because we're dealing with an import path now,
// not an fs path
return filepath.ToSlash(path[len(srcprefix):]), nil

14
fs.go
Просмотреть файл

@ -12,6 +12,7 @@ import (
"runtime"
"syscall"
"github.com/golang/dep/internal"
"github.com/pelletier/go-toml"
"github.com/pkg/errors"
)
@ -32,18 +33,7 @@ func IsRegular(name string) (bool, error) {
}
func IsDir(name string) (bool, error) {
// TODO: lstat?
fi, err := os.Stat(name)
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, err
}
if !fi.IsDir() {
return false, errors.Errorf("%q is not a directory", name)
}
return true, nil
return internal.IsDir(name)
}
func IsNonEmptyDir(name string) (bool, error) {

158
internal/fs.go Normal file
Просмотреть файл

@ -0,0 +1,158 @@
// 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 internal
import (
"os"
"path/filepath"
"strings"
"unicode"
"github.com/pkg/errors"
)
func IsDir(name string) (bool, error) {
// TODO: lstat?
fi, err := os.Stat(name)
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, err
}
if !fi.IsDir() {
return false, errors.Errorf("%q is not a directory", name)
}
return true, nil
}
// HasFilepathPrefix will determine if "path" starts with "prefix" from
// the point of view of a filesystem.
//
// Unlike filepath.HasPrefix, this function is path-aware, meaning that
// it knows that two directories /foo and /foobar are not the same
// thing, and therefore HasFilepathPrefix("/foobar", "/foo") will return
// false.
//
// This function also handles the case where the involved filesystems
// are case-insensitive, meaning /foo/bar and /Foo/Bar correspond to the
// same file. In that situation HasFilepathPrefix("/Foo/Bar", "/foo")
// will return true. The implementation is *not* OS-specific, so a FAT32
// filesystem mounted on Linux will be handled correctly.
func HasFilepathPrefix(path, prefix string) bool {
if filepath.VolumeName(path) != filepath.VolumeName(prefix) {
return false
}
var dn string
if isDir, err := IsDir(path); err != nil {
return false
} else if isDir {
dn = path
} else {
dn = filepath.Dir(path)
}
dn = strings.TrimSuffix(dn, string(os.PathSeparator))
prefix = strings.TrimSuffix(prefix, string(os.PathSeparator))
dirs := strings.Split(dn, string(os.PathSeparator))[1:]
prefixes := strings.Split(prefix, string(os.PathSeparator))[1:]
if len(prefixes) > len(dirs) {
return false
}
var d, p string
for i := range prefixes {
// need to test each component of the path for
// case-sensitiveness because on Unix we could have
// something like ext4 filesystem mounted on FAT
// mountpoint, mounted on ext4 filesystem, i.e. the
// problematic filesystem is not the last one.
if isCaseSensitiveFilesystem(filepath.Join(d, dirs[i])) {
d = filepath.Join(d, dirs[i])
p = filepath.Join(p, prefixes[i])
} else {
d = filepath.Join(d, strings.ToLower(dirs[i]))
p = filepath.Join(p, strings.ToLower(prefixes[i]))
}
if p != d {
return false
}
}
return true
}
// genTestFilename returns a string with at most one rune case-flipped.
//
// The transformation is applied only to the first rune that can be
// reversibly case-flipped, meaning:
//
// * A lowercase rune for which it's true that lower(upper(r)) == r
// * An uppercase rune for which it's true that upper(lower(r)) == r
//
// All the other runes are left intact.
func genTestFilename(str string) string {
flip := true
return strings.Map(func(r rune) rune {
if flip {
if unicode.IsLower(r) {
u := unicode.ToUpper(r)
if unicode.ToLower(u) == r {
r = u
flip = false
}
} else if unicode.IsUpper(r) {
l := unicode.ToLower(r)
if unicode.ToUpper(l) == r {
r = l
flip = false
}
}
}
return r
}, str)
}
// isCaseSensitiveFilesystem determines if the filesystem where dir
// exists is case sensitive or not.
//
// CAVEAT: this function works by taking the last component of the given
// path and flipping the case of the first letter for which case
// flipping is a reversible operation (/foo/Bar → /foo/bar), then
// testing for the existence of the new filename. There are two
// possibilities:
//
// 1. The alternate filename does not exist. We can conclude that the
// filesystem is case sensitive.
//
// 2. The filename happens to exist. We have to test if the two files
// are the same file (case insensitive file system) or different ones
// (case sensitive filesystem).
//
// If the input directory is such that the last component is composed
// exclusively of case-less codepoints (e.g. numbers), this function will
// return false.
func isCaseSensitiveFilesystem(dir string) bool {
alt := filepath.Join(filepath.Dir(dir),
genTestFilename(filepath.Base(dir)))
dInfo, err := os.Stat(dir)
if err != nil {
return true
}
aInfo, err := os.Stat(alt)
if err != nil {
return true
}
return !os.SameFile(dInfo, aInfo)
}

96
internal/fs_test.go Normal file
Просмотреть файл

@ -0,0 +1,96 @@
// 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 internal
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestHasFilepathPrefix(t *testing.T) {
dir, err := ioutil.TempDir("", "dep")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
cases := []struct {
dir string
prefix string
want bool
}{
{filepath.Join(dir, "a", "b"), filepath.Join(dir), true},
{filepath.Join(dir, "a", "b"), filepath.Join(dir, "a"), true},
{filepath.Join(dir, "a", "b"), filepath.Join(dir, "a", "b"), true},
{filepath.Join(dir, "a", "b"), filepath.Join(dir, "c"), false},
{filepath.Join(dir, "a", "b"), filepath.Join(dir, "a", "d", "b"), false},
{filepath.Join(dir, "a", "b"), filepath.Join(dir, "a", "b2"), false},
{filepath.Join(dir, "ab"), filepath.Join(dir, "a", "b"), false},
{filepath.Join(dir, "ab"), filepath.Join(dir, "a"), false},
{filepath.Join(dir, "123"), filepath.Join(dir, "123"), true},
{filepath.Join(dir, "123"), filepath.Join(dir, "1"), false},
{filepath.Join(dir, "⌘"), filepath.Join(dir, "⌘"), true},
{filepath.Join(dir, "a"), filepath.Join(dir, "⌘"), false},
{filepath.Join(dir, "⌘"), filepath.Join(dir, "a"), false},
}
for _, c := range cases {
err := os.MkdirAll(c.dir, 0755)
if err != nil {
t.Fatal(err)
}
err = os.MkdirAll(c.prefix, 0755)
if err != nil {
t.Fatal(err)
}
got := HasFilepathPrefix(c.dir, c.prefix)
if c.want != got {
t.Fatalf("dir: %q, prefix: %q, expected: %v, got: %v", c.dir, c.prefix, c.want, got)
}
}
}
func TestGenTestFilename(t *testing.T) {
cases := []struct {
str string
want string
}{
{"abc", "Abc"},
{"ABC", "aBC"},
{"AbC", "abC"},
{"αβγ", "Αβγ"},
{"123", "123"},
{"1a2", "1A2"},
{"12a", "12A"},
{"⌘", "⌘"},
}
for _, c := range cases {
got := genTestFilename(c.str)
if c.want != got {
t.Fatalf("str: %q, expected: %q, got: %q", c.str, c.want, got)
}
}
}
func BenchmarkGenTestFilename(b *testing.B) {
cases := []string{
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"αααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααααα",
"11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",
"⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘⌘",
}
for i := 0; i < b.N; i++ {
for _, str := range cases {
genTestFilename(str)
}
}
}