Skip to content

Commit 5760ffc

Browse files
committed
cmd/go/internal/modfetch: do not rely on file system for case sensitivity
Over time there may exist two modules with names that differ only in case. On systems with case-insensitive file systems, we need to make sure those modules do not collide in the download cache. Do this by using the new "safe encoding" for file system paths as well as proxy paths. Fixes #25992. Change-Id: I717a9987a87ad5c6927d063bf30d10d9229498c9 Reviewed-on: https://go-review.googlesource.com/124379 Run-TryBot: Russ Cox <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]>
1 parent 203c165 commit 5760ffc

File tree

9 files changed

+220
-27
lines changed

9 files changed

+220
-27
lines changed

src/cmd/go/internal/modcmd/verify.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"fmt"
1010
"io/ioutil"
1111
"os"
12-
"path/filepath"
1312

1413
"cmd/go/internal/base"
1514
"cmd/go/internal/dirhash"
@@ -30,10 +29,14 @@ func runVerify() {
3029

3130
func verifyMod(mod module.Version) bool {
3231
ok := true
33-
zip := filepath.Join(modfetch.SrcMod, "cache/download", mod.Path, "/@v/", mod.Version+".zip")
34-
_, zipErr := os.Stat(zip)
35-
dir := filepath.Join(modfetch.SrcMod, mod.Path+"@"+mod.Version)
36-
_, dirErr := os.Stat(dir)
32+
zip, zipErr := modfetch.CachePath(mod, "zip")
33+
if zipErr == nil {
34+
_, zipErr = os.Stat(zip)
35+
}
36+
dir, dirErr := modfetch.DownloadDir(mod)
37+
if dirErr == nil {
38+
_, dirErr = os.Stat(dir)
39+
}
3740
data, err := ioutil.ReadFile(zip + "hash")
3841
if err != nil {
3942
if zipErr != nil && os.IsNotExist(zipErr) && dirErr != nil && os.IsNotExist(dirErr) {

src/cmd/go/internal/modfetch/cache.go

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"cmd/go/internal/base"
1717
"cmd/go/internal/modfetch/codehost"
18+
"cmd/go/internal/module"
1819
"cmd/go/internal/par"
1920
"cmd/go/internal/semver"
2021
)
@@ -23,6 +24,48 @@ var QuietLookup bool // do not print about lookups
2324

2425
var SrcMod string // $GOPATH/src/mod; set by package modload
2526

27+
func cacheDir(path string) (string, error) {
28+
if SrcMod == "" {
29+
return "", fmt.Errorf("internal error: modfetch.SrcMod not set")
30+
}
31+
enc, err := module.EncodePath(path)
32+
if err != nil {
33+
return "", err
34+
}
35+
return filepath.Join(SrcMod, "cache/download", enc, "/@v"), nil
36+
}
37+
38+
func CachePath(m module.Version, suffix string) (string, error) {
39+
dir, err := cacheDir(m.Path)
40+
if err != nil {
41+
return "", err
42+
}
43+
if !semver.IsValid(m.Version) {
44+
return "", fmt.Errorf("non-semver module version %q", m.Version)
45+
}
46+
if semver.Canonical(m.Version) != m.Version {
47+
return "", fmt.Errorf("non-canonical module version %q", m.Version)
48+
}
49+
return filepath.Join(dir, m.Version+"."+suffix), nil
50+
}
51+
52+
func DownloadDir(m module.Version) (string, error) {
53+
if SrcMod == "" {
54+
return "", fmt.Errorf("internal error: modfetch.SrcMod not set")
55+
}
56+
enc, err := module.EncodePath(m.Path)
57+
if err != nil {
58+
return "", err
59+
}
60+
if !semver.IsValid(m.Version) {
61+
return "", fmt.Errorf("non-semver module version %q", m.Version)
62+
}
63+
if semver.Canonical(m.Version) != m.Version {
64+
return "", fmt.Errorf("non-canonical module version %q", m.Version)
65+
}
66+
return filepath.Join(SrcMod, enc+"@"+m.Version), nil
67+
}
68+
2669
// A cachingRepo is a cache around an underlying Repo,
2770
// avoiding redundant calls to ModulePath, Versions, Stat, Latest, and GoMod (but not Zip).
2871
// It is also safe for simultaneous use by multiple goroutines
@@ -245,7 +288,11 @@ func readDiskStatByHash(path, rev string) (file string, info *RevInfo, err error
245288
return "", nil, errNotCached
246289
}
247290
rev = rev[:12]
248-
dir, err := os.Open(filepath.Join(SrcMod, "cache/download", path, "@v"))
291+
cdir, err := cacheDir(path)
292+
if err != nil {
293+
return "", nil, errNotCached
294+
}
295+
dir, err := os.Open(cdir)
249296
if err != nil {
250297
return "", nil, errNotCached
251298
}
@@ -296,10 +343,10 @@ func readDiskGoMod(path, rev string) (file string, data []byte, err error) {
296343
// If the read fails, the caller can use
297344
// writeDiskCache(file, data) to write a new cache entry.
298345
func readDiskCache(path, rev, suffix string) (file string, data []byte, err error) {
299-
if !semver.IsValid(rev) || SrcMod == "" {
346+
file, err = CachePath(module.Version{Path: path, Version: rev}, suffix)
347+
if err != nil {
300348
return "", nil, errNotCached
301349
}
302-
file = filepath.Join(SrcMod, "cache/download", path, "@v", rev+"."+suffix)
303350
data, err = ioutil.ReadFile(file)
304351
if err != nil {
305352
return file, nil, errNotCached

src/cmd/go/internal/modfetch/fetch.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,30 @@ func Download(mod module.Version) (dir string, err error) {
4141
err error
4242
}
4343
c := downloadCache.Do(mod, func() interface{} {
44-
modpath := mod.Path + "@" + mod.Version
45-
dir = filepath.Join(SrcMod, modpath)
44+
dir, err := DownloadDir(mod)
45+
if err != nil {
46+
return cached{"", err}
47+
}
4648
if files, _ := ioutil.ReadDir(dir); len(files) == 0 {
47-
zipfile := filepath.Join(SrcMod, "cache/download", mod.Path, "@v", mod.Version+".zip")
49+
zipfile, err := CachePath(mod, "zip")
50+
if err != nil {
51+
return cached{"", err}
52+
}
4853
if _, err := os.Stat(zipfile); err == nil {
4954
// Use it.
5055
// This should only happen if the mod/cache directory is preinitialized
5156
// or if src/mod/path was removed but not src/mod/cache/download.
5257
fmt.Fprintf(os.Stderr, "go: extracting %s %s\n", mod.Path, mod.Version)
5358
} else {
54-
if err := os.MkdirAll(filepath.Join(SrcMod, "cache/download", mod.Path, "@v"), 0777); err != nil {
59+
if err := os.MkdirAll(filepath.Dir(zipfile), 0777); err != nil {
5560
return cached{"", err}
5661
}
5762
fmt.Fprintf(os.Stderr, "go: downloading %s %s\n", mod.Path, mod.Version)
5863
if err := downloadZip(mod, zipfile); err != nil {
5964
return cached{"", err}
6065
}
6166
}
67+
modpath := mod.Path + "@" + mod.Version
6268
if err := Unzip(dir, zipfile, modpath, 0); err != nil {
6369
fmt.Fprintf(os.Stderr, "-> %s\n", err)
6470
return cached{"", err}
@@ -201,7 +207,11 @@ func checkSum(mod module.Version) {
201207
}
202208

203209
// Do the file I/O before acquiring the go.sum lock.
204-
data, err := ioutil.ReadFile(filepath.Join(SrcMod, "cache/download", mod.Path, "@v", mod.Version+".ziphash"))
210+
ziphash, err := CachePath(mod, "ziphash")
211+
if err != nil {
212+
base.Fatalf("go: verifying %s@%s: %v", mod.Path, mod.Version, err)
213+
}
214+
data, err := ioutil.ReadFile(ziphash)
205215
if err != nil {
206216
if os.IsNotExist(err) {
207217
// This can happen if someone does rm -rf GOPATH/src/cache/download. So it goes.
@@ -260,7 +270,11 @@ func Sum(mod module.Version) string {
260270
return ""
261271
}
262272

263-
data, err := ioutil.ReadFile(filepath.Join(SrcMod, "cache/download", mod.Path, "@v", mod.Version+".ziphash"))
273+
ziphash, err := CachePath(mod, "ziphash")
274+
if err != nil {
275+
return ""
276+
}
277+
data, err := ioutil.ReadFile(ziphash)
264278
if err != nil {
265279
return ""
266280
}

src/cmd/go/internal/modfetch/proxy.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"time"
1616

1717
"cmd/go/internal/modfetch/codehost"
18+
"cmd/go/internal/module"
1819
"cmd/go/internal/semver"
1920
)
2021

@@ -26,16 +27,20 @@ func lookupProxy(path string) (Repo, error) {
2627
// Don't echo $GOPROXY back in case it has user:password in it (sigh).
2728
return nil, fmt.Errorf("invalid $GOPROXY setting")
2829
}
29-
return newProxyRepo(u.String(), path), nil
30+
return newProxyRepo(u.String(), path)
3031
}
3132

3233
type proxyRepo struct {
3334
url string
3435
path string
3536
}
3637

37-
func newProxyRepo(baseURL, path string) Repo {
38-
return &proxyRepo{strings.TrimSuffix(baseURL, "/") + "/" + pathEscape(path), path}
38+
func newProxyRepo(baseURL, path string) (Repo, error) {
39+
enc, err := module.EncodePath(path)
40+
if err != nil {
41+
return nil, err
42+
}
43+
return &proxyRepo{strings.TrimSuffix(baseURL, "/") + "/" + pathEscape(enc), path}, nil
3944
}
4045

4146
func (p *proxyRepo) ModulePath() string {

src/cmd/go/internal/modfetch/repo.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ func lookup(path string) (r Repo, err error) {
218218

219219
if rr.VCS == "mod" {
220220
// Fetch module from proxy with base URL rr.Repo.
221-
return newProxyRepo(rr.Repo, path), nil
221+
return newProxyRepo(rr.Repo, path)
222222
}
223223

224224
code, err := lookupCodeRepo(rr)

src/cmd/go/internal/modload/build.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"cmd/go/internal/modinfo"
1313
"cmd/go/internal/module"
1414
"cmd/go/internal/search"
15-
"cmd/go/internal/semver"
1615
"encoding/hex"
1716
"fmt"
1817
"os"
@@ -115,10 +114,9 @@ func moduleInfo(m module.Version, fromBuildList bool) *modinfo.ModulePublic {
115114
m.Version = q.Version
116115
m.Time = &q.Time
117116
}
118-
119-
if semver.IsValid(m.Version) {
120-
dir := filepath.Join(modfetch.SrcMod, m.Path+"@"+m.Version)
121-
if stat, err := os.Stat(dir); err == nil && stat.IsDir() {
117+
dir, err := modfetch.DownloadDir(module.Version{Path: m.Path, Version: m.Version})
118+
if err == nil {
119+
if info, err := os.Stat(dir); err == nil && info.IsDir() {
122120
m.Dir = dir
123121
}
124122
}

src/cmd/go/mod_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,27 @@ func TestModGetUpgrade(t *testing.T) {
807807
tg.grepStderr(`go get: disabled by -getmode=vendor`, "expected disabled")
808808
}
809809

810+
func TestModPathCase(t *testing.T) {
811+
tg := testGoModules(t)
812+
defer tg.cleanup()
813+
814+
tg.run("get", "rsc.io/QUOTE")
815+
816+
tg.run("list", "-m", "all")
817+
tg.grepStdout(`^rsc.io/quote v1.5.2`, "want lower-case quote v1.5.2")
818+
tg.grepStdout(`^rsc.io/QUOTE v1.5.2`, "want upper-case quote v1.5.2")
819+
820+
// Note: the package is rsc.io/QUOTE/QUOTE to avoid
821+
// a case-sensitive import collision error in load/pkg.go.
822+
// Once the module code is checking imports within a module,
823+
// that error should probably e relaxed, so that it's allowed to have
824+
// both x.com/FOO/bar and x.com/foo/bar in the same program
825+
// provided the module paths are x.com/FOO and x.com/foo.
826+
tg.run("list", "-f=DEPS {{.Deps}}\nDIR {{.Dir}}", "rsc.io/QUOTE/QUOTE")
827+
tg.grepStdout(`DEPS.*rsc.io/quote`, "want quote as dep")
828+
tg.grepStdout(`DIR.*!q!u!o!t!e`, "want !q!u!o!t!e in directory name")
829+
}
830+
810831
func TestModBadDomain(t *testing.T) {
811832
tg := testGoModules(t)
812833
defer tg.cleanup()

src/cmd/go/proxy_test.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"path/filepath"
1919
"strings"
2020
"sync"
21+
"testing"
2122

2223
"cmd/go/internal/modfetch"
2324
"cmd/go/internal/modfetch/codehost"
@@ -77,7 +78,12 @@ func readModList() {
7778
if i < 0 {
7879
continue
7980
}
80-
path := strings.Replace(name[:i], "_", "/", -1)
81+
enc := strings.Replace(name[:i], "_", "/", -1)
82+
path, err := module.DecodePath(enc)
83+
if err != nil {
84+
fmt.Fprintf(os.Stderr, "go proxy_test: %v", err)
85+
continue
86+
}
8187
vers := name[i+1:]
8288
modList = append(modList, module.Version{Path: path, Version: vers})
8389
}
@@ -98,7 +104,13 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
98104
http.NotFound(w, r)
99105
return
100106
}
101-
path, file := path[:i], path[i+len("/@v/"):]
107+
enc, file := path[:i], path[i+len("/@v/"):]
108+
path, err := module.DecodePath(enc)
109+
if err != nil {
110+
fmt.Fprintf(os.Stderr, "go proxy_test: %v\n", err)
111+
http.NotFound(w, r)
112+
return
113+
}
102114
if file == "list" {
103115
n := 0
104116
for _, m := range modList {
@@ -218,12 +230,17 @@ func findHash(m module.Version) string {
218230
var archiveCache par.Cache
219231

220232
func readArchive(path, vers string) *txtar.Archive {
221-
prefix := strings.Replace(path, "/", "_", -1)
233+
enc, err := module.EncodePath(path)
234+
if err != nil {
235+
return nil
236+
}
237+
238+
prefix := strings.Replace(enc, "/", "_", -1)
222239
name := filepath.Join(cmdGoDir, "testdata/mod", prefix+"_"+vers+".txt")
223240
a := archiveCache.Do(name, func() interface{} {
224241
a, err := txtar.ParseFile(name)
225242
if err != nil {
226-
if !os.IsNotExist(err) {
243+
if testing.Verbose() || !os.IsNotExist(err) {
227244
fmt.Fprintf(os.Stderr, "go proxy: %v\n", err)
228245
}
229246
a = nil

0 commit comments

Comments
 (0)