Skip to content

Commit 2d41645

Browse files
committed
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
1 parent 61e02ee commit 2d41645

File tree

3 files changed

+140
-1
lines changed

3 files changed

+140
-1
lines changed

src/cmd/go/main.go

+57
Original file line numberDiff line numberDiff line change
@@ -589,3 +589,60 @@ func stringList(args ...interface{}) []string {
589589
}
590590
return x
591591
}
592+
593+
// toFold returns a string with the property that
594+
// strings.EqualFold(s, t) iff toFold(s) == toFold(t)
595+
// This lets us test a large set of strings for fold-equivalent
596+
// duplicates without making a quadratic number of calls
597+
// to EqualFold. Note that strings.ToUpper and strings.ToLower
598+
// have the desired property in some corner cases.
599+
func toFold(s string) string {
600+
// Fast path: all ASCII, no upper case.
601+
// Most paths look like this already.
602+
for i := 0; i < len(s); i++ {
603+
c := s[i]
604+
if c >= utf8.RuneSelf || 'A' <= c && c <= 'Z' {
605+
goto Slow
606+
}
607+
}
608+
return s
609+
610+
Slow:
611+
var buf bytes.Buffer
612+
for _, r := range s {
613+
// SimpleFold(x) cycles to the next equivalent rune > x
614+
// or wraps around to smaller values. Iterate until it wraps,
615+
// and we've found the minimum value.
616+
for {
617+
r0 := r
618+
r = unicode.SimpleFold(r0)
619+
if r <= r0 {
620+
break
621+
}
622+
}
623+
// Exception to allow fast path above: A-Z => a-z
624+
if 'A' <= r && r <= 'Z' {
625+
r += 'a' - 'A'
626+
}
627+
buf.WriteRune(r)
628+
}
629+
return buf.String()
630+
}
631+
632+
// foldDup reports a pair of strings from the list that are
633+
// equal according to strings.EqualFold.
634+
// It returns "", "" if there are no such strings.
635+
func foldDup(list []string) (string, string) {
636+
clash := map[string]string{}
637+
for _, s := range list {
638+
fold := toFold(s)
639+
if t := clash[fold]; t != "" {
640+
if s > t {
641+
s, t = t, s
642+
}
643+
return s, t
644+
}
645+
clash[fold] = s
646+
}
647+
return "", ""
648+
}

src/cmd/go/pkg.go

+42-1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ func (p *PackageError) Error() string {
125125
// is the most important thing.
126126
return p.Pos + ": " + p.Err
127127
}
128+
if len(p.ImportStack) == 0 {
129+
return p.Err
130+
}
128131
return "package " + strings.Join(p.ImportStack, "\n\timports ") + ": " + p.Err
129132
}
130133

@@ -370,6 +373,31 @@ func (p *Package) load(stk *importStack, bp *build.Package, err error) *Package
370373
p.allgofiles = append(p.allgofiles, p.gofiles...)
371374
sort.Strings(p.allgofiles)
372375

376+
// Check for case-insensitive collision of input files.
377+
// To avoid problems on case-insensitive files, we reject any package
378+
// where two different input files have equal names under a case-insensitive
379+
// comparison.
380+
f1, f2 := foldDup(stringList(
381+
p.GoFiles,
382+
p.CgoFiles,
383+
p.IgnoredGoFiles,
384+
p.CFiles,
385+
p.HFiles,
386+
p.SFiles,
387+
p.SysoFiles,
388+
p.SwigFiles,
389+
p.SwigCXXFiles,
390+
p.TestGoFiles,
391+
p.XTestGoFiles,
392+
))
393+
if f1 != "" {
394+
p.Error = &PackageError{
395+
ImportStack: stk.copy(),
396+
Err: fmt.Sprintf("case-insensitive file name collision: %q and %q", f1, f2),
397+
}
398+
return p
399+
}
400+
373401
// Build list of imported packages and full dependency list.
374402
imports := make([]*Package, 0, len(p.Imports))
375403
deps := make(map[string]bool)
@@ -423,8 +451,21 @@ func (p *Package) load(stk *importStack, bp *build.Package, err error) *Package
423451
if p.Standard && (p.ImportPath == "unsafe" || buildContext.Compiler == "gccgo") {
424452
p.target = ""
425453
}
426-
427454
p.Target = p.target
455+
456+
// In the absence of errors lower in the dependency tree,
457+
// check for case-insensitive collisions of import paths.
458+
if len(p.DepsErrors) == 0 {
459+
dep1, dep2 := foldDup(p.Deps)
460+
if dep1 != "" {
461+
p.Error = &PackageError{
462+
ImportStack: stk.copy(),
463+
Err: fmt.Sprintf("case-insensitive import collision: %q and %q", dep1, dep2),
464+
}
465+
return p
466+
}
467+
}
468+
428469
return p
429470
}
430471

src/cmd/go/test.bash

+41
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,47 @@ fi
279279
unset GOPATH
280280
rm -rf $d
281281

282+
# issue 4773. case-insensitive collisions
283+
d=$(mktemp -d -t testgo)
284+
export GOPATH=$d
285+
mkdir -p $d/src/example/a $d/src/example/b
286+
cat >$d/src/example/a/a.go <<EOF
287+
package p
288+
import (
289+
_ "math/rand"
290+
_ "math/Rand"
291+
)
292+
EOF
293+
if ./testgo list example/a 2>$d/out; then
294+
echo go list example/a should have failed, did not.
295+
ok=false
296+
elif ! grep "case-insensitive import collision" $d/out >/dev/null; then
297+
echo go list example/a did not report import collision.
298+
ok=false
299+
fi
300+
cat >$d/src/example/b/file.go <<EOF
301+
package b
302+
EOF
303+
cat >$d/src/example/b/FILE.go <<EOF
304+
package b
305+
EOF
306+
if [ $(ls $d/src/example/b | wc -l) = 2 ]; then
307+
# case-sensitive file system, let directory read find both files
308+
args="example/b"
309+
else
310+
# case-insensitive file system, list files explicitly on command line.
311+
args="$d/src/example/b/file.go $d/src/example/b/FILE.go"
312+
fi
313+
if ./testgo list $args 2>$d/out; then
314+
echo go list example/b should have failed, did not.
315+
ok=false
316+
elif ! grep "case-insensitive file name collision" $d/out >/dev/null; then
317+
echo go list example/b did not report file name collision.
318+
ok=false
319+
fi
320+
unset GOPATH
321+
rm -rf $d
322+
282323
# Only succeeds if source order is preserved.
283324
./testgo test testdata/example[12]_test.go
284325

0 commit comments

Comments
 (0)