summaryrefslogtreecommitdiffstats
path: root/src/cmd/go/internal/modfetch/fetch.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/go/internal/modfetch/fetch.go')
-rw-r--r--src/cmd/go/internal/modfetch/fetch.go980
1 files changed, 980 insertions, 0 deletions
diff --git a/src/cmd/go/internal/modfetch/fetch.go b/src/cmd/go/internal/modfetch/fetch.go
new file mode 100644
index 0000000..dfe5da6
--- /dev/null
+++ b/src/cmd/go/internal/modfetch/fetch.go
@@ -0,0 +1,980 @@
+// Copyright 2018 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 modfetch
+
+import (
+ "archive/zip"
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "sync"
+
+ "cmd/go/internal/base"
+ "cmd/go/internal/cfg"
+ "cmd/go/internal/fsys"
+ "cmd/go/internal/lockedfile"
+ "cmd/go/internal/par"
+ "cmd/go/internal/robustio"
+ "cmd/go/internal/str"
+ "cmd/go/internal/trace"
+
+ "golang.org/x/mod/module"
+ "golang.org/x/mod/sumdb/dirhash"
+ modzip "golang.org/x/mod/zip"
+)
+
+var downloadCache par.Cache
+
+// Download downloads the specific module version to the
+// local download cache and returns the name of the directory
+// corresponding to the root of the module's file tree.
+func Download(ctx context.Context, mod module.Version) (dir string, err error) {
+ if err := checkCacheDir(); err != nil {
+ base.Fatalf("go: %v", err)
+ }
+
+ // The par.Cache here avoids duplicate work.
+ type cached struct {
+ dir string
+ err error
+ }
+ c := downloadCache.Do(mod, func() any {
+ dir, err := download(ctx, mod)
+ if err != nil {
+ return cached{"", err}
+ }
+ checkMod(mod)
+ return cached{dir, nil}
+ }).(cached)
+ return c.dir, c.err
+}
+
+func download(ctx context.Context, mod module.Version) (dir string, err error) {
+ ctx, span := trace.StartSpan(ctx, "modfetch.download "+mod.String())
+ defer span.Done()
+
+ dir, err = DownloadDir(mod)
+ if err == nil {
+ // The directory has already been completely extracted (no .partial file exists).
+ return dir, nil
+ } else if dir == "" || !errors.Is(err, fs.ErrNotExist) {
+ return "", err
+ }
+
+ // To avoid cluttering the cache with extraneous files,
+ // DownloadZip uses the same lockfile as Download.
+ // Invoke DownloadZip before locking the file.
+ zipfile, err := DownloadZip(ctx, mod)
+ if err != nil {
+ return "", err
+ }
+
+ unlock, err := lockVersion(mod)
+ if err != nil {
+ return "", err
+ }
+ defer unlock()
+
+ ctx, span = trace.StartSpan(ctx, "unzip "+zipfile)
+ defer span.Done()
+
+ // Check whether the directory was populated while we were waiting on the lock.
+ _, dirErr := DownloadDir(mod)
+ if dirErr == nil {
+ return dir, nil
+ }
+ _, dirExists := dirErr.(*DownloadDirPartialError)
+
+ // Clean up any remaining temporary directories created by old versions
+ // (before 1.16), as well as partially extracted directories (indicated by
+ // DownloadDirPartialError, usually because of a .partial file). This is only
+ // safe to do because the lock file ensures that their writers are no longer
+ // active.
+ parentDir := filepath.Dir(dir)
+ tmpPrefix := filepath.Base(dir) + ".tmp-"
+ if old, err := filepath.Glob(filepath.Join(str.QuoteGlob(parentDir), str.QuoteGlob(tmpPrefix)+"*")); err == nil {
+ for _, path := range old {
+ RemoveAll(path) // best effort
+ }
+ }
+ if dirExists {
+ if err := RemoveAll(dir); err != nil {
+ return "", err
+ }
+ }
+
+ partialPath, err := CachePath(mod, "partial")
+ if err != nil {
+ return "", err
+ }
+
+ // Extract the module zip directory at its final location.
+ //
+ // To prevent other processes from reading the directory if we crash,
+ // create a .partial file before extracting the directory, and delete
+ // the .partial file afterward (all while holding the lock).
+ //
+ // Before Go 1.16, we extracted to a temporary directory with a random name
+ // then renamed it into place with os.Rename. On Windows, this failed with
+ // ERROR_ACCESS_DENIED when another process (usually an anti-virus scanner)
+ // opened files in the temporary directory.
+ //
+ // Go 1.14.2 and higher respect .partial files. Older versions may use
+ // partially extracted directories. 'go mod verify' can detect this,
+ // and 'go clean -modcache' can fix it.
+ if err := os.MkdirAll(parentDir, 0777); err != nil {
+ return "", err
+ }
+ if err := os.WriteFile(partialPath, nil, 0666); err != nil {
+ return "", err
+ }
+ if err := modzip.Unzip(dir, mod, zipfile); err != nil {
+ fmt.Fprintf(os.Stderr, "-> %s\n", err)
+ if rmErr := RemoveAll(dir); rmErr == nil {
+ os.Remove(partialPath)
+ }
+ return "", err
+ }
+ if err := os.Remove(partialPath); err != nil {
+ return "", err
+ }
+
+ if !cfg.ModCacheRW {
+ makeDirsReadOnly(dir)
+ }
+ return dir, nil
+}
+
+var downloadZipCache par.Cache
+
+// DownloadZip downloads the specific module version to the
+// local zip cache and returns the name of the zip file.
+func DownloadZip(ctx context.Context, mod module.Version) (zipfile string, err error) {
+ // The par.Cache here avoids duplicate work.
+ type cached struct {
+ zipfile string
+ err error
+ }
+ c := downloadZipCache.Do(mod, func() any {
+ zipfile, err := CachePath(mod, "zip")
+ if err != nil {
+ return cached{"", err}
+ }
+ ziphashfile := zipfile + "hash"
+
+ // Return without locking if the zip and ziphash files exist.
+ if _, err := os.Stat(zipfile); err == nil {
+ if _, err := os.Stat(ziphashfile); err == nil {
+ return cached{zipfile, nil}
+ }
+ }
+
+ // The zip or ziphash file does not exist. Acquire the lock and create them.
+ if cfg.CmdName != "mod download" {
+ fmt.Fprintf(os.Stderr, "go: downloading %s %s\n", mod.Path, mod.Version)
+ }
+ unlock, err := lockVersion(mod)
+ if err != nil {
+ return cached{"", err}
+ }
+ defer unlock()
+
+ if err := downloadZip(ctx, mod, zipfile); err != nil {
+ return cached{"", err}
+ }
+ return cached{zipfile, nil}
+ }).(cached)
+ return c.zipfile, c.err
+}
+
+func downloadZip(ctx context.Context, mod module.Version, zipfile string) (err error) {
+ ctx, span := trace.StartSpan(ctx, "modfetch.downloadZip "+zipfile)
+ defer span.Done()
+
+ // Double-check that the zipfile was not created while we were waiting for
+ // the lock in DownloadZip.
+ ziphashfile := zipfile + "hash"
+ var zipExists, ziphashExists bool
+ if _, err := os.Stat(zipfile); err == nil {
+ zipExists = true
+ }
+ if _, err := os.Stat(ziphashfile); err == nil {
+ ziphashExists = true
+ }
+ if zipExists && ziphashExists {
+ return nil
+ }
+
+ // Create parent directories.
+ if err := os.MkdirAll(filepath.Dir(zipfile), 0777); err != nil {
+ return err
+ }
+
+ // Clean up any remaining tempfiles from previous runs.
+ // This is only safe to do because the lock file ensures that their
+ // writers are no longer active.
+ tmpPattern := filepath.Base(zipfile) + "*.tmp"
+ if old, err := filepath.Glob(filepath.Join(str.QuoteGlob(filepath.Dir(zipfile)), tmpPattern)); err == nil {
+ for _, path := range old {
+ os.Remove(path) // best effort
+ }
+ }
+
+ // If the zip file exists, the ziphash file must have been deleted
+ // or lost after a file system crash. Re-hash the zip without downloading.
+ if zipExists {
+ return hashZip(mod, zipfile, ziphashfile)
+ }
+
+ // From here to the os.Rename call below is functionally almost equivalent to
+ // renameio.WriteToFile, with one key difference: we want to validate the
+ // contents of the file (by hashing it) before we commit it. Because the file
+ // is zip-compressed, we need an actual file — or at least an io.ReaderAt — to
+ // validate it: we can't just tee the stream as we write it.
+ f, err := tempFile(filepath.Dir(zipfile), filepath.Base(zipfile), 0666)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err != nil {
+ f.Close()
+ os.Remove(f.Name())
+ }
+ }()
+
+ var unrecoverableErr error
+ err = TryProxies(func(proxy string) error {
+ if unrecoverableErr != nil {
+ return unrecoverableErr
+ }
+ repo := Lookup(proxy, mod.Path)
+ err := repo.Zip(f, mod.Version)
+ if err != nil {
+ // Zip may have partially written to f before failing.
+ // (Perhaps the server crashed while sending the file?)
+ // Since we allow fallback on error in some cases, we need to fix up the
+ // file to be empty again for the next attempt.
+ if _, err := f.Seek(0, io.SeekStart); err != nil {
+ unrecoverableErr = err
+ return err
+ }
+ if err := f.Truncate(0); err != nil {
+ unrecoverableErr = err
+ return err
+ }
+ }
+ return err
+ })
+ if err != nil {
+ return err
+ }
+
+ // Double-check that the paths within the zip file are well-formed.
+ //
+ // TODO(bcmills): There is a similar check within the Unzip function. Can we eliminate one?
+ fi, err := f.Stat()
+ if err != nil {
+ return err
+ }
+ z, err := zip.NewReader(f, fi.Size())
+ if err != nil {
+ return err
+ }
+ prefix := mod.Path + "@" + mod.Version + "/"
+ for _, f := range z.File {
+ if !strings.HasPrefix(f.Name, prefix) {
+ return fmt.Errorf("zip for %s has unexpected file %s", prefix[:len(prefix)-1], f.Name)
+ }
+ }
+
+ if err := f.Close(); err != nil {
+ return err
+ }
+
+ // Hash the zip file and check the sum before renaming to the final location.
+ if err := hashZip(mod, f.Name(), ziphashfile); err != nil {
+ return err
+ }
+ if err := os.Rename(f.Name(), zipfile); err != nil {
+ return err
+ }
+
+ // TODO(bcmills): Should we make the .zip and .ziphash files read-only to discourage tampering?
+
+ return nil
+}
+
+// hashZip reads the zip file opened in f, then writes the hash to ziphashfile,
+// overwriting that file if it exists.
+//
+// If the hash does not match go.sum (or the sumdb if enabled), hashZip returns
+// an error and does not write ziphashfile.
+func hashZip(mod module.Version, zipfile, ziphashfile string) (err error) {
+ hash, err := dirhash.HashZip(zipfile, dirhash.DefaultHash)
+ if err != nil {
+ return err
+ }
+ if err := checkModSum(mod, hash); err != nil {
+ return err
+ }
+ hf, err := lockedfile.Create(ziphashfile)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if closeErr := hf.Close(); err == nil && closeErr != nil {
+ err = closeErr
+ }
+ }()
+ if err := hf.Truncate(int64(len(hash))); err != nil {
+ return err
+ }
+ if _, err := hf.WriteAt([]byte(hash), 0); err != nil {
+ return err
+ }
+ return nil
+}
+
+// makeDirsReadOnly makes a best-effort attempt to remove write permissions for dir
+// and its transitive contents.
+func makeDirsReadOnly(dir string) {
+ type pathMode struct {
+ path string
+ mode fs.FileMode
+ }
+ var dirs []pathMode // in lexical order
+ filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
+ if err == nil && d.IsDir() {
+ info, err := d.Info()
+ if err == nil && info.Mode()&0222 != 0 {
+ dirs = append(dirs, pathMode{path, info.Mode()})
+ }
+ }
+ return nil
+ })
+
+ // Run over list backward to chmod children before parents.
+ for i := len(dirs) - 1; i >= 0; i-- {
+ os.Chmod(dirs[i].path, dirs[i].mode&^0222)
+ }
+}
+
+// RemoveAll removes a directory written by Download or Unzip, first applying
+// any permission changes needed to do so.
+func RemoveAll(dir string) error {
+ // Module cache has 0555 directories; make them writable in order to remove content.
+ filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
+ if err != nil {
+ return nil // ignore errors walking in file system
+ }
+ if info.IsDir() {
+ os.Chmod(path, 0777)
+ }
+ return nil
+ })
+ return robustio.RemoveAll(dir)
+}
+
+var GoSumFile string // path to go.sum; set by package modload
+var WorkspaceGoSumFiles []string // path to module go.sums in workspace; set by package modload
+
+type modSum struct {
+ mod module.Version
+ sum string
+}
+
+var goSum struct {
+ mu sync.Mutex
+ m map[module.Version][]string // content of go.sum file
+ w map[string]map[module.Version][]string // sum file in workspace -> content of that sum file
+ status map[modSum]modSumStatus // state of sums in m
+ overwrite bool // if true, overwrite go.sum without incorporating its contents
+ enabled bool // whether to use go.sum at all
+}
+
+type modSumStatus struct {
+ used, dirty bool
+}
+
+// Reset resets globals in the modfetch package, so previous loads don't affect
+// contents of go.sum files.
+func Reset() {
+ GoSumFile = ""
+ WorkspaceGoSumFiles = nil
+
+ // Uses of lookupCache and downloadCache both can call checkModSum,
+ // which in turn sets the used bit on goSum.status for modules.
+ // Reset them so used can be computed properly.
+ lookupCache = par.Cache{}
+ downloadCache = par.Cache{}
+
+ // Clear all fields on goSum. It will be initialized later
+ goSum.mu.Lock()
+ goSum.m = nil
+ goSum.w = nil
+ goSum.status = nil
+ goSum.overwrite = false
+ goSum.enabled = false
+ goSum.mu.Unlock()
+}
+
+// initGoSum initializes the go.sum data.
+// The boolean it returns reports whether the
+// use of go.sum is now enabled.
+// The goSum lock must be held.
+func initGoSum() (bool, error) {
+ if GoSumFile == "" {
+ return false, nil
+ }
+ if goSum.m != nil {
+ return true, nil
+ }
+
+ goSum.m = make(map[module.Version][]string)
+ goSum.status = make(map[modSum]modSumStatus)
+ goSum.w = make(map[string]map[module.Version][]string)
+
+ for _, f := range WorkspaceGoSumFiles {
+ goSum.w[f] = make(map[module.Version][]string)
+ _, err := readGoSumFile(goSum.w[f], f)
+ if err != nil {
+ return false, err
+ }
+ }
+
+ enabled, err := readGoSumFile(goSum.m, GoSumFile)
+ goSum.enabled = enabled
+ return enabled, err
+}
+
+func readGoSumFile(dst map[module.Version][]string, file string) (bool, error) {
+ var (
+ data []byte
+ err error
+ )
+ if actualSumFile, ok := fsys.OverlayPath(file); ok {
+ // Don't lock go.sum if it's part of the overlay.
+ // On Plan 9, locking requires chmod, and we don't want to modify any file
+ // in the overlay. See #44700.
+ data, err = os.ReadFile(actualSumFile)
+ } else {
+ data, err = lockedfile.Read(file)
+ }
+ if err != nil && !os.IsNotExist(err) {
+ return false, err
+ }
+ readGoSum(dst, file, data)
+
+ return true, nil
+}
+
+// emptyGoModHash is the hash of a 1-file tree containing a 0-length go.mod.
+// A bug caused us to write these into go.sum files for non-modules.
+// We detect and remove them.
+const emptyGoModHash = "h1:G7mAYYxgmS0lVkHyy2hEOLQCFB0DlQFTMLWggykrydY="
+
+// readGoSum parses data, which is the content of file,
+// and adds it to goSum.m. The goSum lock must be held.
+func readGoSum(dst map[module.Version][]string, file string, data []byte) error {
+ lineno := 0
+ for len(data) > 0 {
+ var line []byte
+ lineno++
+ i := bytes.IndexByte(data, '\n')
+ if i < 0 {
+ line, data = data, nil
+ } else {
+ line, data = data[:i], data[i+1:]
+ }
+ f := strings.Fields(string(line))
+ if len(f) == 0 {
+ // blank line; skip it
+ continue
+ }
+ if len(f) != 3 {
+ return fmt.Errorf("malformed go.sum:\n%s:%d: wrong number of fields %v", file, lineno, len(f))
+ }
+ if f[2] == emptyGoModHash {
+ // Old bug; drop it.
+ continue
+ }
+ mod := module.Version{Path: f[0], Version: f[1]}
+ dst[mod] = append(dst[mod], f[2])
+ }
+ return nil
+}
+
+// HaveSum returns true if the go.sum file contains an entry for mod.
+// The entry's hash must be generated with a known hash algorithm.
+// mod.Version may have a "/go.mod" suffix to distinguish sums for
+// .mod and .zip files.
+func HaveSum(mod module.Version) bool {
+ goSum.mu.Lock()
+ defer goSum.mu.Unlock()
+ inited, err := initGoSum()
+ if err != nil || !inited {
+ return false
+ }
+ for _, goSums := range goSum.w {
+ for _, h := range goSums[mod] {
+ if !strings.HasPrefix(h, "h1:") {
+ continue
+ }
+ if !goSum.status[modSum{mod, h}].dirty {
+ return true
+ }
+ }
+ }
+ for _, h := range goSum.m[mod] {
+ if !strings.HasPrefix(h, "h1:") {
+ continue
+ }
+ if !goSum.status[modSum{mod, h}].dirty {
+ return true
+ }
+ }
+ return false
+}
+
+// checkMod checks the given module's checksum.
+func checkMod(mod module.Version) {
+ // Do the file I/O before acquiring the go.sum lock.
+ ziphash, err := CachePath(mod, "ziphash")
+ if err != nil {
+ base.Fatalf("verifying %v", module.VersionError(mod, err))
+ }
+ data, err := lockedfile.Read(ziphash)
+ if err != nil {
+ base.Fatalf("verifying %v", module.VersionError(mod, err))
+ }
+ data = bytes.TrimSpace(data)
+ if !isValidSum(data) {
+ // Recreate ziphash file from zip file and use that to check the mod sum.
+ zip, err := CachePath(mod, "zip")
+ if err != nil {
+ base.Fatalf("verifying %v", module.VersionError(mod, err))
+ }
+ err = hashZip(mod, zip, ziphash)
+ if err != nil {
+ base.Fatalf("verifying %v", module.VersionError(mod, err))
+ }
+ return
+ }
+ h := string(data)
+ if !strings.HasPrefix(h, "h1:") {
+ base.Fatalf("verifying %v", module.VersionError(mod, fmt.Errorf("unexpected ziphash: %q", h)))
+ }
+
+ if err := checkModSum(mod, h); err != nil {
+ base.Fatalf("%s", err)
+ }
+}
+
+// goModSum returns the checksum for the go.mod contents.
+func goModSum(data []byte) (string, error) {
+ return dirhash.Hash1([]string{"go.mod"}, func(string) (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewReader(data)), nil
+ })
+}
+
+// checkGoMod checks the given module's go.mod checksum;
+// data is the go.mod content.
+func checkGoMod(path, version string, data []byte) error {
+ h, err := goModSum(data)
+ if err != nil {
+ return &module.ModuleError{Path: path, Version: version, Err: fmt.Errorf("verifying go.mod: %v", err)}
+ }
+
+ return checkModSum(module.Version{Path: path, Version: version + "/go.mod"}, h)
+}
+
+// checkModSum checks that the recorded checksum for mod is h.
+//
+// mod.Version may have the additional suffix "/go.mod" to request the checksum
+// for the module's go.mod file only.
+func checkModSum(mod module.Version, h string) error {
+ // We lock goSum when manipulating it,
+ // but we arrange to release the lock when calling checkSumDB,
+ // so that parallel calls to checkModHash can execute parallel calls
+ // to checkSumDB.
+
+ // Check whether mod+h is listed in go.sum already. If so, we're done.
+ goSum.mu.Lock()
+ inited, err := initGoSum()
+ if err != nil {
+ goSum.mu.Unlock()
+ return err
+ }
+ done := inited && haveModSumLocked(mod, h)
+ if inited {
+ st := goSum.status[modSum{mod, h}]
+ st.used = true
+ goSum.status[modSum{mod, h}] = st
+ }
+ goSum.mu.Unlock()
+
+ if done {
+ return nil
+ }
+
+ // Not listed, so we want to add them.
+ // Consult checksum database if appropriate.
+ if useSumDB(mod) {
+ // Calls base.Fatalf if mismatch detected.
+ if err := checkSumDB(mod, h); err != nil {
+ return err
+ }
+ }
+
+ // Add mod+h to go.sum, if it hasn't appeared already.
+ if inited {
+ goSum.mu.Lock()
+ addModSumLocked(mod, h)
+ st := goSum.status[modSum{mod, h}]
+ st.dirty = true
+ goSum.status[modSum{mod, h}] = st
+ goSum.mu.Unlock()
+ }
+ return nil
+}
+
+// haveModSumLocked reports whether the pair mod,h is already listed in go.sum.
+// If it finds a conflicting pair instead, it calls base.Fatalf.
+// goSum.mu must be locked.
+func haveModSumLocked(mod module.Version, h string) bool {
+ sumFileName := "go.sum"
+ if strings.HasSuffix(GoSumFile, "go.work.sum") {
+ sumFileName = "go.work.sum"
+ }
+ for _, vh := range goSum.m[mod] {
+ if h == vh {
+ return true
+ }
+ if strings.HasPrefix(vh, "h1:") {
+ base.Fatalf("verifying %s@%s: checksum mismatch\n\tdownloaded: %v\n\t%s: %v"+goSumMismatch, mod.Path, mod.Version, h, sumFileName, vh)
+ }
+ }
+ // Also check workspace sums.
+ foundMatch := false
+ // Check sums from all files in case there are conflicts between
+ // the files.
+ for goSumFile, goSums := range goSum.w {
+ for _, vh := range goSums[mod] {
+ if h == vh {
+ foundMatch = true
+ } else if strings.HasPrefix(vh, "h1:") {
+ base.Fatalf("verifying %s@%s: checksum mismatch\n\tdownloaded: %v\n\t%s: %v"+goSumMismatch, mod.Path, mod.Version, h, goSumFile, vh)
+ }
+ }
+ }
+ return foundMatch
+}
+
+// addModSumLocked adds the pair mod,h to go.sum.
+// goSum.mu must be locked.
+func addModSumLocked(mod module.Version, h string) {
+ if haveModSumLocked(mod, h) {
+ return
+ }
+ if len(goSum.m[mod]) > 0 {
+ fmt.Fprintf(os.Stderr, "warning: verifying %s@%s: unknown hashes in go.sum: %v; adding %v"+hashVersionMismatch, mod.Path, mod.Version, strings.Join(goSum.m[mod], ", "), h)
+ }
+ goSum.m[mod] = append(goSum.m[mod], h)
+}
+
+// checkSumDB checks the mod, h pair against the Go checksum database.
+// It calls base.Fatalf if the hash is to be rejected.
+func checkSumDB(mod module.Version, h string) error {
+ modWithoutSuffix := mod
+ noun := "module"
+ if before, found := strings.CutSuffix(mod.Version, "/go.mod"); found {
+ noun = "go.mod"
+ modWithoutSuffix.Version = before
+ }
+
+ db, lines, err := lookupSumDB(mod)
+ if err != nil {
+ return module.VersionError(modWithoutSuffix, fmt.Errorf("verifying %s: %v", noun, err))
+ }
+
+ have := mod.Path + " " + mod.Version + " " + h
+ prefix := mod.Path + " " + mod.Version + " h1:"
+ for _, line := range lines {
+ if line == have {
+ return nil
+ }
+ if strings.HasPrefix(line, prefix) {
+ return module.VersionError(modWithoutSuffix, fmt.Errorf("verifying %s: checksum mismatch\n\tdownloaded: %v\n\t%s: %v"+sumdbMismatch, noun, h, db, line[len(prefix)-len("h1:"):]))
+ }
+ }
+ return nil
+}
+
+// Sum returns the checksum for the downloaded copy of the given module,
+// if present in the download cache.
+func Sum(mod module.Version) string {
+ if cfg.GOMODCACHE == "" {
+ // Do not use current directory.
+ return ""
+ }
+
+ ziphash, err := CachePath(mod, "ziphash")
+ if err != nil {
+ return ""
+ }
+ data, err := lockedfile.Read(ziphash)
+ if err != nil {
+ return ""
+ }
+ data = bytes.TrimSpace(data)
+ if !isValidSum(data) {
+ return ""
+ }
+ return string(data)
+}
+
+// isValidSum returns true if data is the valid contents of a zip hash file.
+// Certain critical files are written to disk by first truncating
+// then writing the actual bytes, so that if the write fails
+// the corrupt file should contain at least one of the null
+// bytes written by the truncate operation.
+func isValidSum(data []byte) bool {
+ if bytes.IndexByte(data, '\000') >= 0 {
+ return false
+ }
+
+ if len(data) != len("h1:")+base64.StdEncoding.EncodedLen(sha256.Size) {
+ return false
+ }
+
+ return true
+}
+
+var ErrGoSumDirty = errors.New("updates to go.sum needed, disabled by -mod=readonly")
+
+// WriteGoSum writes the go.sum file if it needs to be updated.
+//
+// keep is used to check whether a newly added sum should be saved in go.sum.
+// It should have entries for both module content sums and go.mod sums
+// (version ends with "/go.mod"). Existing sums will be preserved unless they
+// have been marked for deletion with TrimGoSum.
+func WriteGoSum(keep map[module.Version]bool, readonly bool) error {
+ goSum.mu.Lock()
+ defer goSum.mu.Unlock()
+
+ // If we haven't read the go.sum file yet, don't bother writing it.
+ if !goSum.enabled {
+ return nil
+ }
+
+ // Check whether we need to add sums for which keep[m] is true or remove
+ // unused sums marked with TrimGoSum. If there are no changes to make,
+ // just return without opening go.sum.
+ dirty := false
+Outer:
+ for m, hs := range goSum.m {
+ for _, h := range hs {
+ st := goSum.status[modSum{m, h}]
+ if st.dirty && (!st.used || keep[m]) {
+ dirty = true
+ break Outer
+ }
+ }
+ }
+ if !dirty {
+ return nil
+ }
+ if readonly {
+ return ErrGoSumDirty
+ }
+ if _, ok := fsys.OverlayPath(GoSumFile); ok {
+ base.Fatalf("go: updates to go.sum needed, but go.sum is part of the overlay specified with -overlay")
+ }
+
+ // Make a best-effort attempt to acquire the side lock, only to exclude
+ // previous versions of the 'go' command from making simultaneous edits.
+ if unlock, err := SideLock(); err == nil {
+ defer unlock()
+ }
+
+ err := lockedfile.Transform(GoSumFile, func(data []byte) ([]byte, error) {
+ if !goSum.overwrite {
+ // Incorporate any sums added by other processes in the meantime.
+ // Add only the sums that we actually checked: the user may have edited or
+ // truncated the file to remove erroneous hashes, and we shouldn't restore
+ // them without good reason.
+ goSum.m = make(map[module.Version][]string, len(goSum.m))
+ readGoSum(goSum.m, GoSumFile, data)
+ for ms, st := range goSum.status {
+ if st.used && !sumInWorkspaceModulesLocked(ms.mod) {
+ addModSumLocked(ms.mod, ms.sum)
+ }
+ }
+ }
+
+ var mods []module.Version
+ for m := range goSum.m {
+ mods = append(mods, m)
+ }
+ module.Sort(mods)
+
+ var buf bytes.Buffer
+ for _, m := range mods {
+ list := goSum.m[m]
+ sort.Strings(list)
+ str.Uniq(&list)
+ for _, h := range list {
+ st := goSum.status[modSum{m, h}]
+ if (!st.dirty || (st.used && keep[m])) && !sumInWorkspaceModulesLocked(m) {
+ fmt.Fprintf(&buf, "%s %s %s\n", m.Path, m.Version, h)
+ }
+ }
+ }
+ return buf.Bytes(), nil
+ })
+
+ if err != nil {
+ return fmt.Errorf("updating go.sum: %w", err)
+ }
+
+ goSum.status = make(map[modSum]modSumStatus)
+ goSum.overwrite = false
+ return nil
+}
+
+func sumInWorkspaceModulesLocked(m module.Version) bool {
+ for _, goSums := range goSum.w {
+ if _, ok := goSums[m]; ok {
+ return true
+ }
+ }
+ return false
+}
+
+// TrimGoSum trims go.sum to contain only the modules needed for reproducible
+// builds.
+//
+// keep is used to check whether a sum should be retained in go.mod. It should
+// have entries for both module content sums and go.mod sums (version ends
+// with "/go.mod").
+func TrimGoSum(keep map[module.Version]bool) {
+ goSum.mu.Lock()
+ defer goSum.mu.Unlock()
+ inited, err := initGoSum()
+ if err != nil {
+ base.Fatalf("%s", err)
+ }
+ if !inited {
+ return
+ }
+
+ for m, hs := range goSum.m {
+ if !keep[m] {
+ for _, h := range hs {
+ goSum.status[modSum{m, h}] = modSumStatus{used: false, dirty: true}
+ }
+ goSum.overwrite = true
+ }
+ }
+}
+
+const goSumMismatch = `
+
+SECURITY ERROR
+This download does NOT match an earlier download recorded in go.sum.
+The bits may have been replaced on the origin server, or an attacker may
+have intercepted the download attempt.
+
+For more information, see 'go help module-auth'.
+`
+
+const sumdbMismatch = `
+
+SECURITY ERROR
+This download does NOT match the one reported by the checksum server.
+The bits may have been replaced on the origin server, or an attacker may
+have intercepted the download attempt.
+
+For more information, see 'go help module-auth'.
+`
+
+const hashVersionMismatch = `
+
+SECURITY WARNING
+This download is listed in go.sum, but using an unknown hash algorithm.
+The download cannot be verified.
+
+For more information, see 'go help module-auth'.
+
+`
+
+var HelpModuleAuth = &base.Command{
+ UsageLine: "module-auth",
+ Short: "module authentication using go.sum",
+ Long: `
+When the go command downloads a module zip file or go.mod file into the
+module cache, it computes a cryptographic hash and compares it with a known
+value to verify the file hasn't changed since it was first downloaded. Known
+hashes are stored in a file in the module root directory named go.sum. Hashes
+may also be downloaded from the checksum database depending on the values of
+GOSUMDB, GOPRIVATE, and GONOSUMDB.
+
+For details, see https://golang.org/ref/mod#authenticating.
+`,
+}
+
+var HelpPrivate = &base.Command{
+ UsageLine: "private",
+ Short: "configuration for downloading non-public code",
+ Long: `
+The go command defaults to downloading modules from the public Go module
+mirror at proxy.golang.org. It also defaults to validating downloaded modules,
+regardless of source, against the public Go checksum database at sum.golang.org.
+These defaults work well for publicly available source code.
+
+The GOPRIVATE environment variable controls which modules the go command
+considers to be private (not available publicly) and should therefore not use
+the proxy or checksum database. The variable is a comma-separated list of
+glob patterns (in the syntax of Go's path.Match) of module path prefixes.
+For example,
+
+ GOPRIVATE=*.corp.example.com,rsc.io/private
+
+causes the go command to treat as private any module with a path prefix
+matching either pattern, including git.corp.example.com/xyzzy, rsc.io/private,
+and rsc.io/private/quux.
+
+For fine-grained control over module download and validation, the GONOPROXY
+and GONOSUMDB environment variables accept the same kind of glob list
+and override GOPRIVATE for the specific decision of whether to use the proxy
+and checksum database, respectively.
+
+For example, if a company ran a module proxy serving private modules,
+users would configure go using:
+
+ GOPRIVATE=*.corp.example.com
+ GOPROXY=proxy.example.com
+ GONOPROXY=none
+
+The GOPRIVATE variable is also used to define the "public" and "private"
+patterns for the GOVCS variable; see 'go help vcs'. For that usage,
+GOPRIVATE applies even in GOPATH mode. In that case, it matches import paths
+instead of module paths.
+
+The 'go env -w' command (see 'go help env') can be used to set these variables
+for future go command invocations.
+
+For more details, see https://golang.org/ref/mod#private-modules.
+`,
+}