blob: 18c23fc62d19658f7aaaddaa32888a1395c1585e [file] [log] [blame]
// Copyright 2019 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 fetch
import (
"archive/zip"
"bytes"
"context"
"errors"
"fmt"
"go/ast"
"go/build"
"go/parser"
"go/token"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"runtime"
"runtime/debug"
"sort"
"strings"
"time"
"github.com/google/safehtml/template"
"go.opencensus.io/trace"
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/fetch/dochtml"
"golang.org/x/pkgsite/internal/fetch/internal/doc"
"golang.org/x/pkgsite/internal/licenses"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/proxy"
"golang.org/x/pkgsite/internal/source"
"golang.org/x/pkgsite/internal/stdlib"
"golang.org/x/pkgsite/internal/version"
)
var (
errModuleContainsNoPackages = errors.New("module contains 0 packages")
errMalformedZip = errors.New("module zip is malformed")
)
type FetchResult struct {
ModulePath string
RequestedVersion string
ResolvedVersion string
GoModPath string
Status int
Error error
Module *internal.Module
PackageVersionStates []*internal.PackageVersionState
}
// FetchModule queries the proxy or the Go repo for the requested module
// version, downloads the module zip, and processes the contents to return an
// *internal.Module and related information.
//
// Even if err is non-nil, the result may contain useful information, like the go.mod path.
func FetchModule(ctx context.Context, modulePath, requestedVersion string, proxyClient *proxy.Client, sourceClient *source.Client) (fr *FetchResult) {
fr = &FetchResult{
ModulePath: modulePath,
RequestedVersion: requestedVersion,
}
defer func() {
if fr.Error != nil {
derrors.Wrap(&fr.Error, "FetchModule(%q, %q)", modulePath, requestedVersion)
fr.Status = derrors.ToStatus(fr.Error)
}
if fr.Status == 0 {
fr.Status = http.StatusOK
}
log.Debugf(ctx, "memory after fetch of %s@%s: %dM", modulePath, requestedVersion, allocMeg())
}()
var (
commitTime time.Time
zipReader *zip.Reader
err error
)
if modulePath == stdlib.ModulePath {
zipReader, commitTime, err = stdlib.Zip(requestedVersion)
if err != nil {
fr.Error = err
return fr
}
fr.GoModPath = stdlib.ModulePath
fr.ResolvedVersion = requestedVersion
} else {
info, err := proxyClient.GetInfo(ctx, modulePath, requestedVersion)
if err != nil {
fr.Error = err
return fr
}
fr.ResolvedVersion = info.Version
commitTime = info.Time
goModBytes, err := proxyClient.GetMod(ctx, modulePath, fr.ResolvedVersion)
if err != nil {
fr.Error = err
return fr
}
goModPath := modfile.ModulePath(goModBytes)
if goModPath == "" {
fr.Error = fmt.Errorf("go.mod has no module path: %w", derrors.BadModule)
return fr
}
fr.GoModPath = goModPath
if goModPath != modulePath {
// The module path in the go.mod file doesn't match the path of the
// zip file. Don't insert the module. Store an AlternativeModule
// status in module_version_states.
fr.Error = fmt.Errorf("module path=%s, go.mod path=%s: %w", modulePath, goModPath, derrors.AlternativeModule)
return fr
}
zipReader, err = proxyClient.GetZip(ctx, modulePath, fr.ResolvedVersion)
if err != nil {
fr.Error = err
return fr
}
}
versionType, err := version.ParseType(fr.ResolvedVersion)
if err != nil {
fr.Error = fmt.Errorf("%v: %w", err, derrors.BadModule)
return fr
}
mod, pvs, err := processZipFile(ctx, modulePath, versionType, fr.ResolvedVersion, commitTime, zipReader, sourceClient)
if err != nil {
fr.Error = err
return fr
}
fr.Module = mod
fr.PackageVersionStates = pvs
if modulePath == stdlib.ModulePath {
fr.Module.HasGoMod = true
}
for _, state := range fr.PackageVersionStates {
if state.Status != http.StatusOK {
fr.Status = derrors.ToStatus(derrors.HasIncompletePackages)
}
}
return fr
}
// processZipFile extracts information from the module version zip.
func processZipFile(ctx context.Context, modulePath string, versionType version.Type, resolvedVersion string, commitTime time.Time, zipReader *zip.Reader, sourceClient *source.Client) (_ *internal.Module, _ []*internal.PackageVersionState, err error) {
defer derrors.Wrap(&err, "processZipFile(%q, %q)", modulePath, resolvedVersion)
ctx, span := trace.StartSpan(ctx, "fetch.processZipFile")
defer span.End()
sourceInfo, err := source.ModuleInfo(ctx, sourceClient, modulePath, resolvedVersion)
if err != nil {
log.Infof(ctx, "error getting source info: %v", err)
}
readmes, err := extractReadmesFromZip(modulePath, resolvedVersion, zipReader)
if err != nil {
return nil, nil, fmt.Errorf("extractReadmesFromZip(%q, %q, zipReader): %v", modulePath, resolvedVersion, err)
}
logf := func(format string, args ...interface{}) {
log.Infof(ctx, format, args...)
}
d := licenses.NewDetector(modulePath, resolvedVersion, zipReader, logf)
allLicenses := d.AllLicenses()
packages, packageVersionStates, err := extractPackagesFromZip(ctx, modulePath, resolvedVersion, zipReader, d, sourceInfo)
if errors.Is(err, errModuleContainsNoPackages) || errors.Is(err, errMalformedZip) {
return nil, nil, fmt.Errorf("%v: %w", err.Error(), derrors.BadModule)
}
if err != nil {
return nil, nil, fmt.Errorf("extractPackagesFromZip(%q, %q, zipReader, %v): %v", modulePath, resolvedVersion, allLicenses, err)
}
hasGoMod := zipContainsFilename(zipReader, path.Join(moduleVersionDir(modulePath, resolvedVersion), "go.mod"))
var readmeFilePath, readmeContents string
for _, r := range readmes {
if path.Dir(r.Filepath) != "." {
continue
}
readmeFilePath = r.Filepath
readmeContents = r.Contents
break
}
return &internal.Module{
LegacyModuleInfo: internal.LegacyModuleInfo{
ModuleInfo: internal.ModuleInfo{
ModulePath: modulePath,
Version: resolvedVersion,
CommitTime: commitTime,
VersionType: versionType,
IsRedistributable: d.ModuleIsRedistributable(),
HasGoMod: hasGoMod,
SourceInfo: sourceInfo,
},
LegacyReadmeFilePath: readmeFilePath,
LegacyReadmeContents: readmeContents,
},
LegacyPackages: packages,
Licenses: allLicenses,
Directories: moduleDirectories(modulePath, packages, readmes, d),
}, packageVersionStates, nil
}
// moduleVersionDir formats the content subdirectory for the given
// modulePath and version.
func moduleVersionDir(modulePath, version string) string {
return fmt.Sprintf("%s@%s", modulePath, version)
}
// extractReadmesFromZip returns the file path and contents of all files from r
// that are README files.
func extractReadmesFromZip(modulePath, resolvedVersion string, r *zip.Reader) ([]*internal.Readme, error) {
var readmes []*internal.Readme
for _, zipFile := range r.File {
if isReadme(zipFile.Name) {
if zipFile.UncompressedSize64 > MaxFileSize {
return nil, fmt.Errorf("file size %d exceeds max limit %d", zipFile.UncompressedSize64, MaxFileSize)
}
c, err := readZipFile(zipFile)
if err != nil {
return nil, err
}
readmes = append(readmes, &internal.Readme{
Filepath: strings.TrimPrefix(zipFile.Name, moduleVersionDir(modulePath, resolvedVersion)+"/"),
Contents: string(c),
})
}
}
return readmes, nil
}
// isReadme reports whether file is README or if the base name of file, with or
// without the extension, is equal to expectedFile. README.go files will return
// false. It is case insensitive. It operates on '/'-separated paths.
func isReadme(file string) bool {
base := path.Base(file)
if strings.EqualFold(base, "README.go") {
return false
}
const expectedFile = "README"
return strings.EqualFold(file, expectedFile) ||
strings.EqualFold(base, expectedFile) ||
strings.EqualFold(strings.TrimSuffix(base, path.Ext(base)), expectedFile)
}
// extractPackagesFromZip returns a slice of packages from the module zip r.
// It matches against the given licenses to determine the subset of licenses
// that applies to each package.
// The second return value says whether any packages are "incomplete," meaning
// that they contained .go files but couldn't be processed due to current
// limitations of this site. The limitations are:
// * a maximum file size (MaxFileSize)
// * the particular set of build contexts we consider (goEnvs)
// * whether the import path is valid.
func extractPackagesFromZip(ctx context.Context, modulePath, resolvedVersion string, r *zip.Reader, d *licenses.Detector, sourceInfo *source.Info) (_ []*internal.LegacyPackage, _ []*internal.PackageVersionState, err error) {
ctx, span := trace.StartSpan(ctx, "fetch.extractPackagesFromZip")
defer span.End()
defer func() {
if e := recover(); e != nil {
// The package processing code performs some sanity checks along the way.
// None of the panics should occur, but if they do, we want to log them and
// be able to find them. So, convert internal panics to internal errors here.
err = fmt.Errorf("internal panic: %v\n\n%s", e, debug.Stack())
}
}()
// The high-level approach is to split the processing of the zip file
// into two phases:
//
// 1. loop over all files, looking at file metadata only
// 2. process all files by reading their contents
//
// During phase 1, we populate the dirs map for each directory
// that contains at least one .go file.
var (
// modulePrefix is the "<module>@<resolvedVersion>/" prefix that all files
// are expected to have according to the zip archive layout specification
// at the bottom of https://golang.org/cmd/go/#hdr-Module_proxy_protocol.
modulePrefix = moduleVersionDir(modulePath, resolvedVersion) + "/"
// dirs is the set of directories with at least one .go file,
// to be populated during phase 1 and used during phase 2.
//
// The map key is the directory path, with the modulePrefix trimmed.
// The map value is a slice of all .go files, and no other files.
dirs = make(map[string][]*zip.File)
// modInfo contains all the module information a package in the module
// needs to render its documentation, to be populated during phase 1
// and used during phase 2.
modInfo = &dochtml.ModuleInfo{
ModulePath: modulePath,
ResolvedVersion: resolvedVersion,
ModulePackages: make(map[string]bool),
}
// incompleteDirs tracks directories for which we have incomplete
// information, due to a problem processing one of the go files contained
// therein. We use this so that a single unprocessable package does not
// prevent processing of other packages in the module.
incompleteDirs = make(map[string]bool)
packageVersionStates = []*internal.PackageVersionState{}
)
// Phase 1.
// Loop over zip files preemptively and check for problems
// that can be detected by looking at metadata alone.
// We'll be looking at file contents starting with phase 2 only,
// only after we're sure this phase passed without errors.
for _, f := range r.File {
if f.Mode().IsDir() {
// While "go mod download" will never put a directory in a zip, any can serve their
// own zips. Example: go.felesatra.moe/binpack@v0.1.0.
// Directory entries are harmless, so we just ignore them.
continue
}
if !strings.HasPrefix(f.Name, modulePrefix) {
// Well-formed module zips have all files under modulePrefix.
return nil, nil, fmt.Errorf("expected file to have prefix %q; got = %q: %w",
modulePrefix, f.Name, errMalformedZip)
}
innerPath := path.Dir(f.Name[len(modulePrefix):])
if incompleteDirs[innerPath] {
// We already know this directory cannot be processed, so skip.
continue
}
importPath := path.Join(modulePath, innerPath)
if ignoredByGoTool(importPath) || isVendored(importPath) {
// File is in a directory we're not looking to process at this time, so skip it.
continue
}
if !strings.HasSuffix(f.Name, ".go") {
// We care about .go files only.
continue
}
// It's possible to have a Go package in a directory that does not result in a valid import path.
// That package cannot be imported, but that may be fine if it's a main package, intended to built
// and run from that directory.
// Example: https://github.com/postmannen/go-learning/blob/master/concurrency/01-sending%20numbers%20and%20receving%20numbers%20from%20a%20channel/main.go
// We're not set up to handle invalid import paths, so skip these packages.
if err := module.CheckImportPath(importPath); err != nil {
incompleteDirs[innerPath] = true
packageVersionStates = append(packageVersionStates, &internal.PackageVersionState{
ModulePath: modulePath,
PackagePath: importPath,
Version: resolvedVersion,
Status: derrors.ToStatus(derrors.PackageBadImportPath),
Error: err.Error(),
})
continue
}
if f.UncompressedSize64 > MaxFileSize {
incompleteDirs[innerPath] = true
status := derrors.ToStatus(derrors.PackageMaxFileSizeLimitExceeded)
err := fmt.Sprintf("Unable to process %s: file size %d exceeds max limit %d",
f.Name, f.UncompressedSize64, MaxFileSize)
packageVersionStates = append(packageVersionStates, &internal.PackageVersionState{
ModulePath: modulePath,
PackagePath: importPath,
Version: resolvedVersion,
Status: status,
Error: err,
})
continue
}
dirs[innerPath] = append(dirs[innerPath], f)
if len(dirs) > maxPackagesPerModule {
return nil, nil, fmt.Errorf("%d packages found in %q; exceeds limit %d for maxPackagePerModule", len(dirs), modulePath, maxPackagesPerModule)
}
}
for pkgName := range dirs {
modInfo.ModulePackages[path.Join(modulePath, pkgName)] = true
}
// Phase 2.
// If we got this far, the file metadata was okay.
// Start reading the file contents now to extract information
// about Go packages.
var pkgs []*internal.LegacyPackage
for innerPath, goFiles := range dirs {
if incompleteDirs[innerPath] {
// Something went wrong when processing this directory, so we skip.
log.Infof(ctx, "Skipping %q because it is incomplete", innerPath)
continue
}
var (
status error
errMsg string
)
pkg, err := loadPackage(ctx, goFiles, innerPath, sourceInfo, modInfo)
if bpe := (*BadPackageError)(nil); errors.As(err, &bpe) {
incompleteDirs[innerPath] = true
status = derrors.PackageInvalidContents
errMsg = err.Error()
} else if errors.Is(err, dochtml.ErrTooLarge) {
status = derrors.PackageDocumentationHTMLTooLarge
errMsg = err.Error()
} else if err != nil {
return nil, nil, fmt.Errorf("unexpected error loading package: %v", err)
}
var pkgPath string
if pkg == nil {
// No package.
if len(goFiles) > 0 {
// There were go files, but no build contexts matched them.
incompleteDirs[innerPath] = true
status = derrors.PackageBuildContextNotSupported
}
pkgPath = path.Join(modulePath, innerPath)
} else {
if d != nil { // should only be nil for tests
isRedist, lics := d.PackageInfo(innerPath)
pkg.IsRedistributable = isRedist
for _, l := range lics {
pkg.Licenses = append(pkg.Licenses, l.Metadata)
}
}
pkgs = append(pkgs, pkg)
pkgPath = pkg.Path
}
code := http.StatusOK
if status != nil {
code = derrors.ToStatus(status)
}
packageVersionStates = append(packageVersionStates, &internal.PackageVersionState{
ModulePath: modulePath,
PackagePath: pkgPath,
Version: resolvedVersion,
Status: code,
Error: errMsg,
})
}
if len(pkgs) == 0 {
return nil, packageVersionStates, errModuleContainsNoPackages
}
return pkgs, packageVersionStates, nil
}
// ignoredByGoTool reports whether the given import path corresponds
// to a directory that would be ignored by the go tool.
//
// The logic of the go tool for ignoring directories is documented at
// https://golang.org/cmd/go/#hdr-Package_lists_and_patterns:
//
// LegacyDirectory and file names that begin with "." or "_" are ignored
// by the go tool, as are directories named "testdata".
//
func ignoredByGoTool(importPath string) bool {
for _, el := range strings.Split(importPath, "/") {
if strings.HasPrefix(el, ".") || strings.HasPrefix(el, "_") || el == "testdata" {
return true
}
}
return false
}
// isVendored reports whether the given import path corresponds
// to a Go package that is inside a vendor directory.
//
// The logic for what is considered a vendor directory is documented at
// https://golang.org/cmd/go/#hdr-Vendor_Directories.
func isVendored(importPath string) bool {
return strings.HasPrefix(importPath, "vendor/") ||
strings.Contains(importPath, "/vendor/")
}
// zipContainsFilename reports whether there is a file with the given name in the zip.
func zipContainsFilename(r *zip.Reader, name string) bool {
for _, f := range r.File {
if f.Name == name {
return true
}
}
return false
}
// BadPackageError represents an error loading a package
// because its contents do not make up a valid package.
//
// This can happen, for example, if the .go files fail
// to parse or declare different package names.
type BadPackageError struct {
Err error // Not nil.
}
func (bpe *BadPackageError) Error() string { return bpe.Err.Error() }
// Go environments used to construct build contexts in loadPackage.
var goEnvs = []struct{ GOOS, GOARCH string }{
{"linux", "amd64"},
{"windows", "amd64"},
{"darwin", "amd64"},
{"js", "wasm"},
{"linux", "js"},
}
// loadPackage loads a Go package by calling loadPackageWithBuildContext, trying
// several build contexts in turn. The first build context in the list to produce
// a non-empty package is used. If none of them result in a package, then
// loadPackage returns nil, nil.
//
// If the package is fine except that its documentation is too large, loadPackage
// returns both a package and a non-nil error with dochtml.ErrTooLarge in its chain.
func loadPackage(ctx context.Context, zipGoFiles []*zip.File, innerPath string, sourceInfo *source.Info, modInfo *dochtml.ModuleInfo) (*internal.LegacyPackage, error) {
ctx, span := trace.StartSpan(ctx, "fetch.loadPackage")
defer span.End()
for _, env := range goEnvs {
pkg, err := loadPackageWithBuildContext(ctx, env.GOOS, env.GOARCH, zipGoFiles, innerPath, sourceInfo, modInfo)
if err != nil && !errors.Is(err, dochtml.ErrTooLarge) {
return nil, err
}
if pkg != nil {
return pkg, err
}
}
return nil, nil
}
// httpPost allows package fetch tests to stub out playground URL fetches.
var httpPost = http.Post
const docTooLargeReplacement = `<p>Documentation is too large to display.</p>`
// loadPackageWithBuildContext loads a Go package made of .go files in zipGoFiles
// using a build context constructed from the given GOOS and GOARCH values.
// modulePath is stdlib.ModulePath for the Go standard library and the module
// path for all other modules. innerPath is the path of the Go package directory
// relative to the module root.
//
// zipGoFiles must contain only .go files that have been verified
// to be of reasonable size.
//
// The returned LegacyPackage.Licenses field is not populated.
//
// It returns a nil LegacyPackage if the directory doesn't contain a Go package
// or all .go files have been excluded by constraints.
// A *BadPackageError error is returned if the directory
// contains .go files but do not make up a valid package.
func loadPackageWithBuildContext(ctx context.Context, goos, goarch string, zipGoFiles []*zip.File, innerPath string, sourceInfo *source.Info, modInfo *dochtml.ModuleInfo) (_ *internal.LegacyPackage, err error) {
modulePath := modInfo.ModulePath
defer derrors.Wrap(&err, "loadPackageWithBuildContext(%q, %q, zipGoFiles, %q, %q, %+v)",
goos, goarch, innerPath, modulePath, sourceInfo)
// Apply build constraints to get a map from matching file names to their contents.
files, err := matchingFiles(goos, goarch, zipGoFiles)
if err != nil {
return nil, err
}
// Parse .go files and add them to the goFiles slice.
var (
fset = token.NewFileSet()
goFiles = make(map[string]*ast.File)
allGoFiles []*ast.File
packageName string
packageNameFile string // Name of file where packageName came from.
)
for name, b := range files {
pf, err := parser.ParseFile(fset, name, b, parser.ParseComments)
if err != nil {
if pf == nil {
return nil, fmt.Errorf("internal error: the source couldn't be read: %v", err)
}
return nil, &BadPackageError{Err: err}
}
allGoFiles = append(allGoFiles, pf)
if strings.HasSuffix(name, "_test.go") {
continue
}
goFiles[name] = pf
if len(goFiles) == 1 {
packageName = pf.Name.Name
packageNameFile = name
} else if pf.Name.Name != packageName {
return nil, &BadPackageError{Err: &build.MultiplePackageError{
Dir: innerPath,
Packages: []string{packageName, pf.Name.Name},
Files: []string{packageNameFile, name},
}}
}
}
if len(goFiles) == 0 {
// This directory doesn't contain a package, or at least not one
// that matches this build context.
return nil, nil
}
// The "builtin" package in the standard library is a special case.
// We want to show documentation for all globals (not just exported ones),
// and avoid association of consts, vars, and factory functions with types
// since it's not helpful (see golang.org/issue/6645).
var noFiltering, noTypeAssociation bool
if modulePath == stdlib.ModulePath && innerPath == "builtin" {
noFiltering = true
noTypeAssociation = true
}
// Compute package documentation.
importPath := path.Join(modulePath, innerPath)
var m doc.Mode
if noFiltering {
m |= doc.AllDecls
}
d, err := doc.NewFromFiles(fset, allGoFiles, importPath, m)
if err != nil {
return nil, fmt.Errorf("doc.NewFromFiles: %v", err)
}
if d.ImportPath != importPath || d.Name != packageName {
panic(fmt.Errorf("internal error: *doc.Package has an unexpected import path (%q != %q) or package name (%q != %q)", d.ImportPath, importPath, d.Name, packageName))
}
if noTypeAssociation {
for _, t := range d.Types {
d.Consts, t.Consts = append(d.Consts, t.Consts...), nil
d.Vars, t.Vars = append(d.Vars, t.Vars...), nil
d.Funcs, t.Funcs = append(d.Funcs, t.Funcs...), nil
}
sort.Slice(d.Funcs, func(i, j int) bool { return d.Funcs[i].Name < d.Funcs[j].Name })
}
// Process package imports.
if len(d.Imports) > maxImportsPerPackage {
return nil, fmt.Errorf("%d imports found package %q; exceeds limit %d for maxImportsPerPackage", len(d.Imports), importPath, maxImportsPerPackage)
}
// Render documentation HTML.
sourceLinkFunc := func(n ast.Node) string {
if sourceInfo == nil {
return ""
}
p := fset.Position(n.Pos())
if p.Line == 0 { // invalid Position
return ""
}
return sourceInfo.LineURL(path.Join(innerPath, p.Filename), p.Line)
}
fileLinkFunc := func(filename string) string {
if sourceInfo == nil {
return ""
}
return sourceInfo.FileURL(path.Join(innerPath, filename))
}
docHTML, err := dochtml.Render(ctx, fset, d, dochtml.RenderOptions{
FileLinkFunc: fileLinkFunc,
SourceLinkFunc: sourceLinkFunc,
ModInfo: modInfo,
Limit: int64(MaxDocumentationHTML),
})
if errors.Is(err, dochtml.ErrTooLarge) {
docHTML = template.MustParseAndExecuteToHTML(docTooLargeReplacement)
} else if err != nil {
return nil, fmt.Errorf("dochtml.Render: %v", err)
}
v1path := internal.V1Path(modulePath, innerPath)
if modulePath == stdlib.ModulePath {
importPath = innerPath
}
return &internal.LegacyPackage{
Path: importPath,
Name: packageName,
Synopsis: doc.Synopsis(d.Doc),
V1Path: v1path,
Imports: d.Imports,
DocumentationHTML: docHTML,
GOOS: goos,
GOARCH: goarch,
}, err
}
// matchingFiles returns a map from file names to their contents, read from zipGoFiles.
// It includes only those files that match the build context determined by goos and goarch.
func matchingFiles(goos, goarch string, zipGoFiles []*zip.File) (files map[string][]byte, err error) {
defer derrors.Wrap(&err, "matchingFiles(%q, %q, zipGoFiles)", goos, goarch)
// Populate the map with all the zip files.
files = make(map[string][]byte)
for _, f := range zipGoFiles {
_, name := path.Split(f.Name)
b, err := readZipFile(f)
if err != nil {
return nil, err
}
files[name] = b
}
// bctx is used to make decisions about which of the .go files are included
// by build constraints.
bctx := &build.Context{
GOOS: goos,
GOARCH: goarch,
CgoEnabled: true,
Compiler: build.Default.Compiler,
ReleaseTags: build.Default.ReleaseTags,
JoinPath: path.Join,
OpenFile: func(name string) (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader(files[name])), nil
},
// If left nil, the default implementations of these read from disk,
// which we do not want. None of these functions should be used
// inside this function; it would be an internal error if they are.
// Set them to non-nil values to catch if that happens.
SplitPathList: func(string) []string { panic("internal error: unexpected call to SplitPathList") },
IsAbsPath: func(string) bool { panic("internal error: unexpected call to IsAbsPath") },
IsDir: func(string) bool { panic("internal error: unexpected call to IsDir") },
HasSubdir: func(string, string) (string, bool) { panic("internal error: unexpected call to HasSubdir") },
ReadDir: func(string) ([]os.FileInfo, error) { panic("internal error: unexpected call to ReadDir") },
}
for name := range files {
match, err := bctx.MatchFile(".", name) // This will access the file we just added to files map above.
if err != nil {
return nil, &BadPackageError{Err: fmt.Errorf(`bctx.MatchFile(".", %q): %w`, name, err)}
}
if !match {
// Excluded by build context.
delete(files, name)
}
}
return files, nil
}
// readZipFile decompresses zip file f and returns its uncompressed contents.
// The caller can check f.UncompressedSize64 before calling readZipFile to
// get the expected uncompressed size of f.
func readZipFile(f *zip.File) (_ []byte, err error) {
defer derrors.Add(&err, "readZipFile(%q)", f.Name)
r, err := f.Open()
if err != nil {
return nil, fmt.Errorf("f.Open(): %v", err)
}
b, err := ioutil.ReadAll(r)
if err != nil {
r.Close()
return nil, fmt.Errorf("ioutil.ReadAll(r): %v", err)
}
if err := r.Close(); err != nil {
return nil, fmt.Errorf("closing: %v", err)
}
return b, nil
}
func allocMeg() int {
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
return int(ms.Alloc / (1024 * 1024))
}