Commit 2d416459 authored by Russ Cox's avatar Russ Cox

cmd/go: reject case-insensitive file name, import collisions

To make sure that Go code will work when moved to a
system with a case-insensitive file system, like OS X or Windows,
reject any package built from files with names differing
only in case, and also any package built from imported
dependencies with names differing only in case.

Fixes #4773.

R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/7314104
parent 61e02ee9
...@@ -589,3 +589,60 @@ func stringList(args ...interface{}) []string { ...@@ -589,3 +589,60 @@ func stringList(args ...interface{}) []string {
} }
return x return x
} }
// toFold returns a string with the property that
// strings.EqualFold(s, t) iff toFold(s) == toFold(t)
// This lets us test a large set of strings for fold-equivalent
// duplicates without making a quadratic number of calls
// to EqualFold. Note that strings.ToUpper and strings.ToLower
// have the desired property in some corner cases.
func toFold(s string) string {
// Fast path: all ASCII, no upper case.
// Most paths look like this already.
for i := 0; i < len(s); i++ {
c := s[i]
if c >= utf8.RuneSelf || 'A' <= c && c <= 'Z' {
goto Slow
}
}
return s
Slow:
var buf bytes.Buffer
for _, r := range s {
// SimpleFold(x) cycles to the next equivalent rune > x
// or wraps around to smaller values. Iterate until it wraps,
// and we've found the minimum value.
for {
r0 := r
r = unicode.SimpleFold(r0)
if r <= r0 {
break
}
}
// Exception to allow fast path above: A-Z => a-z
if 'A' <= r && r <= 'Z' {
r += 'a' - 'A'
}
buf.WriteRune(r)
}
return buf.String()
}
// foldDup reports a pair of strings from the list that are
// equal according to strings.EqualFold.
// It returns "", "" if there are no such strings.
func foldDup(list []string) (string, string) {
clash := map[string]string{}
for _, s := range list {
fold := toFold(s)
if t := clash[fold]; t != "" {
if s > t {
s, t = t, s
}
return s, t
}
clash[fold] = s
}
return "", ""
}
...@@ -125,6 +125,9 @@ func (p *PackageError) Error() string { ...@@ -125,6 +125,9 @@ func (p *PackageError) Error() string {
// is the most important thing. // is the most important thing.
return p.Pos + ": " + p.Err return p.Pos + ": " + p.Err
} }
if len(p.ImportStack) == 0 {
return p.Err
}
return "package " + strings.Join(p.ImportStack, "\n\timports ") + ": " + p.Err return "package " + strings.Join(p.ImportStack, "\n\timports ") + ": " + p.Err
} }
...@@ -370,6 +373,31 @@ func (p *Package) load(stk *importStack, bp *build.Package, err error) *Package ...@@ -370,6 +373,31 @@ func (p *Package) load(stk *importStack, bp *build.Package, err error) *Package
p.allgofiles = append(p.allgofiles, p.gofiles...) p.allgofiles = append(p.allgofiles, p.gofiles...)
sort.Strings(p.allgofiles) sort.Strings(p.allgofiles)
// Check for case-insensitive collision of input files.
// To avoid problems on case-insensitive files, we reject any package
// where two different input files have equal names under a case-insensitive
// comparison.
f1, f2 := foldDup(stringList(
p.GoFiles,
p.CgoFiles,
p.IgnoredGoFiles,
p.CFiles,
p.HFiles,
p.SFiles,
p.SysoFiles,
p.SwigFiles,
p.SwigCXXFiles,
p.TestGoFiles,
p.XTestGoFiles,
))
if f1 != "" {
p.Error = &PackageError{
ImportStack: stk.copy(),
Err: fmt.Sprintf("case-insensitive file name collision: %q and %q", f1, f2),
}
return p
}
// Build list of imported packages and full dependency list. // Build list of imported packages and full dependency list.
imports := make([]*Package, 0, len(p.Imports)) imports := make([]*Package, 0, len(p.Imports))
deps := make(map[string]bool) deps := make(map[string]bool)
...@@ -423,8 +451,21 @@ func (p *Package) load(stk *importStack, bp *build.Package, err error) *Package ...@@ -423,8 +451,21 @@ func (p *Package) load(stk *importStack, bp *build.Package, err error) *Package
if p.Standard && (p.ImportPath == "unsafe" || buildContext.Compiler == "gccgo") { if p.Standard && (p.ImportPath == "unsafe" || buildContext.Compiler == "gccgo") {
p.target = "" p.target = ""
} }
p.Target = p.target p.Target = p.target
// In the absence of errors lower in the dependency tree,
// check for case-insensitive collisions of import paths.
if len(p.DepsErrors) == 0 {
dep1, dep2 := foldDup(p.Deps)
if dep1 != "" {
p.Error = &PackageError{
ImportStack: stk.copy(),
Err: fmt.Sprintf("case-insensitive import collision: %q and %q", dep1, dep2),
}
return p
}
}
return p return p
} }
......
...@@ -279,6 +279,47 @@ fi ...@@ -279,6 +279,47 @@ fi
unset GOPATH unset GOPATH
rm -rf $d rm -rf $d
# issue 4773. case-insensitive collisions
d=$(mktemp -d -t testgo)
export GOPATH=$d
mkdir -p $d/src/example/a $d/src/example/b
cat >$d/src/example/a/a.go <<EOF
package p
import (
_ "math/rand"
_ "math/Rand"
)
EOF
if ./testgo list example/a 2>$d/out; then
echo go list example/a should have failed, did not.
ok=false
elif ! grep "case-insensitive import collision" $d/out >/dev/null; then
echo go list example/a did not report import collision.
ok=false
fi
cat >$d/src/example/b/file.go <<EOF
package b
EOF
cat >$d/src/example/b/FILE.go <<EOF
package b
EOF
if [ $(ls $d/src/example/b | wc -l) = 2 ]; then
# case-sensitive file system, let directory read find both files
args="example/b"
else
# case-insensitive file system, list files explicitly on command line.
args="$d/src/example/b/file.go $d/src/example/b/FILE.go"
fi
if ./testgo list $args 2>$d/out; then
echo go list example/b should have failed, did not.
ok=false
elif ! grep "case-insensitive file name collision" $d/out >/dev/null; then
echo go list example/b did not report file name collision.
ok=false
fi
unset GOPATH
rm -rf $d
# Only succeeds if source order is preserved. # Only succeeds if source order is preserved.
./testgo test testdata/example[12]_test.go ./testgo test testdata/example[12]_test.go
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment