| // 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 provides a way to fetch modules from a proxy. |
| package fetch |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "io/fs" |
| "net/http" |
| "strings" |
| |
| "golang.org/x/mod/modfile" |
| "golang.org/x/pkgsite/internal" |
| "golang.org/x/pkgsite/internal/derrors" |
| "golang.org/x/pkgsite/internal/godoc" |
| "golang.org/x/pkgsite/internal/licenses" |
| "golang.org/x/pkgsite/internal/log" |
| "golang.org/x/pkgsite/internal/proxy" |
| "golang.org/x/pkgsite/internal/stdlib" |
| "golang.org/x/pkgsite/internal/trace" |
| ) |
| |
| var ErrModuleContainsNoPackages = errors.New("module contains 0 packages") |
| |
| type FetchResult struct { |
| ModulePath string |
| RequestedVersion string |
| ResolvedVersion string |
| // HasGoMod says whether the zip contain a go.mod file. If Module (below) is non-nil, then |
| // Module.HasGoMod will be the same value. But HasGoMod will be populated even if Module is nil |
| // because there were problems with it, as long as we can download and read the zip. |
| HasGoMod bool |
| GoModPath string |
| Status int |
| Error error |
| Module *internal.Module |
| PackageVersionStates []*internal.PackageVersionState |
| } |
| |
| // A LazyModule contains the information needed to compute a FetchResult, |
| // but has only done enough work to compute the UnitMetas in the module. |
| // It provides a Unit method to compute a single unit or a fetchResult |
| // method to compute the whole FetchResult. |
| type LazyModule struct { |
| internal.ModuleInfo |
| UnitMetas []*internal.UnitMeta |
| goModPath string |
| requestedVersion string |
| failedPackages []*internal.PackageVersionState |
| licenseDetector *licenses.Detector |
| contentDir fs.FS |
| godocModInfo *godoc.ModuleInfo |
| Error error |
| } |
| |
| // 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, mg ModuleGetter) (fr *FetchResult) { |
| lm := FetchLazyModule(ctx, modulePath, requestedVersion, mg) |
| return lm.fetchResult(ctx) |
| } |
| |
| // FetchLazyModule queries the proxy or the Go repo for the requested module |
| // version, downloads the module zip, and does just enough processing to produce |
| // UnitMetas for all the modules. The full units are computed as needed. |
| func FetchLazyModule(ctx context.Context, modulePath, requestedVersion string, mg ModuleGetter) *LazyModule { |
| lm, err := fetchLazyModule(ctx, modulePath, requestedVersion, mg) |
| if err != nil { |
| lm.Error = err |
| } |
| return lm |
| } |
| |
| func fetchLazyModule(ctx context.Context, modulePath, requestedVersion string, mg ModuleGetter) (*LazyModule, error) { |
| lm := &LazyModule{ |
| requestedVersion: requestedVersion, |
| } |
| lm.ModuleInfo.ModulePath = modulePath |
| |
| info, err := GetInfo(ctx, modulePath, requestedVersion, mg) |
| if err != nil { |
| return lm, err |
| } |
| lm.ModuleInfo.Version = info.Version |
| commitTime := info.Time |
| |
| var contentDir fs.FS |
| switch mg.(type) { |
| case *stdlibZipModuleGetter: |
| // Special behavior for stdlibZipModuleGetter because its info doesn't actually |
| // give us the true resolved version. |
| var resolvedVersion string |
| contentDir, resolvedVersion, commitTime, err = stdlib.ContentDir(ctx, requestedVersion) |
| if err != nil { |
| return lm, err |
| } |
| // If the requested version is a branch name like "master" or "main", we cannot |
| // determine the right resolved version until we start working with the repo. |
| lm.ModuleInfo.Version = resolvedVersion |
| default: |
| contentDir, err = mg.ContentDir(ctx, modulePath, lm.ModuleInfo.Version) |
| if err != nil { |
| return lm, err |
| } |
| } |
| lm.ModuleInfo.CommitTime = commitTime |
| lm.contentDir = contentDir |
| |
| if modulePath == stdlib.ModulePath { |
| lm.ModuleInfo.HasGoMod = true |
| } else { |
| lm.ModuleInfo.HasGoMod = hasGoModFile(contentDir) |
| } |
| |
| // getGoModPath may return a non-empty goModPath even if the error is |
| // non-nil, if the module version is an alternative module. |
| var goModBytes []byte |
| lm.goModPath, goModBytes, err = getGoModPath(ctx, modulePath, lm.ModuleInfo.Version, mg) |
| if err != nil { |
| return lm, err |
| } |
| |
| // If there is no go.mod file in the zip, try other ways to detect |
| // alternative modules: |
| // 1. Compare the module path to a list of known alternative module paths. |
| // 2. Compare the zip signature to a list of known ones to see if this is a |
| // fork. The intent is to avoid processing certain known large modules, not |
| // to find every fork. |
| if !lm.ModuleInfo.HasGoMod { |
| if modPath := knownAlternativeFor(modulePath); modPath != "" { |
| return lm, fmt.Errorf("known alternative to %s: %w", modPath, derrors.AlternativeModule) |
| } |
| forkedModule, err := forkedFrom(contentDir, modulePath, lm.ModuleInfo.Version) |
| if err != nil { |
| return lm, err |
| } |
| if forkedModule != "" { |
| return lm, fmt.Errorf("forked from %s: %w", forkedModule, derrors.AlternativeModule) |
| } |
| } |
| |
| // populate the rest of lm.ModuleInfo before calling extractUnitMetas with it. |
| v := lm.ModuleInfo.Version // version to use for SourceInfo and licenses.NewDetectorFS |
| if _, ok := mg.(*stdlibZipModuleGetter); ok { |
| if modulePath == stdlib.ModulePath && stdlib.SupportedBranches[requestedVersion] { |
| v = requestedVersion |
| } |
| } |
| lm.ModuleInfo.SourceInfo, err = mg.SourceInfo(ctx, modulePath, v) |
| if err != nil { |
| log.Infof(ctx, "error getting source info: %v", err) |
| } |
| logf := func(format string, args ...any) { |
| log.Infof(ctx, format, args...) |
| } |
| lm.licenseDetector = licenses.NewDetectorFS(modulePath, v, contentDir, logf) |
| lm.ModuleInfo.IsRedistributable = lm.licenseDetector.ModuleIsRedistributable() |
| lm.UnitMetas, lm.godocModInfo, lm.failedPackages, err = extractUnitMetas(ctx, lm.ModuleInfo, contentDir) |
| if err != nil { |
| return lm, err |
| } |
| if goModBytes != nil { |
| if err := processGoModFile(goModBytes, &lm.ModuleInfo); err != nil { |
| return lm, fmt.Errorf("%v: %w", err, derrors.BadModule) |
| } |
| } |
| |
| return lm, nil |
| } |
| |
| func (lm *LazyModule) Unit(ctx context.Context, path string) (*internal.Unit, error) { |
| var unitMeta *internal.UnitMeta |
| for _, um := range lm.UnitMetas { |
| if um.Path == path { |
| unitMeta = um |
| } |
| } |
| u, _, err := lm.unit(ctx, unitMeta) |
| if err == nil && u == nil { |
| return nil, fmt.Errorf("unit %v does not exist in module", path) |
| } |
| return u, err |
| } |
| |
| // unit returns the Unit for the given path. It also returns a packageVersionState representing |
| // the state of the work of computing the Unit after the LazyModule was computed. PackageVersionStates |
| // representing packages that failed while the LazyModule was computed are set on the LazyModule. |
| func (lm *LazyModule) unit(ctx context.Context, unitMeta *internal.UnitMeta) (*internal.Unit, *internal.PackageVersionState, error) { |
| readme, err := extractReadme(lm.ModulePath, unitMeta.Path, lm.ModuleInfo.Version, lm.contentDir) |
| if err != nil { |
| return nil, nil, err |
| } |
| // This unit represents the module itself, not a package. |
| if !unitMeta.IsPackage() { |
| return moduleUnit(lm.ModulePath, unitMeta, nil, readme, lm.licenseDetector), nil, nil |
| } |
| pkg, pvs, err := extractPackage(ctx, lm.ModulePath, unitMeta.Path, lm.contentDir, lm.licenseDetector, lm.SourceInfo, lm.godocModInfo) |
| if err != nil || (pvs != nil && pvs.Status != 200) { |
| // pvs can be non-nil even if err is non-nil. |
| return nil, pvs, err |
| } |
| |
| u := moduleUnit(lm.ModulePath, unitMeta, pkg, readme, lm.licenseDetector) |
| return u, pvs, nil |
| } |
| |
| func (lm *LazyModule) fetchResult(ctx context.Context) *FetchResult { |
| fr := &FetchResult{ |
| ModulePath: lm.ModulePath, |
| RequestedVersion: lm.requestedVersion, |
| ResolvedVersion: lm.ModuleInfo.Version, |
| Module: &internal.Module{ |
| ModuleInfo: lm.ModuleInfo, |
| }, |
| HasGoMod: lm.HasGoMod, |
| GoModPath: lm.goModPath, |
| } |
| if lm.Error != nil { |
| fr.Error = lm.Error |
| fr.Status = derrors.ToStatus(lm.Error) |
| if fr.Status == 0 { |
| fr.Status = http.StatusOK |
| } |
| return fr |
| } |
| fr.Module.Licenses = lm.licenseDetector.AllLicenses() |
| // We need to set HasGoMod here rather than on the ModuleInfo when |
| // it's created because the ModuleInfo that goes on the units shouldn't |
| // have HasGoMod set on it. |
| packageVersionStates := append([]*internal.PackageVersionState{}, lm.failedPackages...) |
| for _, um := range lm.UnitMetas { |
| unit, pvs, err := lm.unit(ctx, um) |
| if err != nil { |
| fr.Error = err |
| } |
| if pvs != nil && um.IsPackage() { |
| packageVersionStates = append(packageVersionStates, pvs) |
| } |
| if unit == nil { |
| // No unit was produced but we still had a useful pvs. |
| continue |
| } |
| fr.Module.Units = append(fr.Module.Units, unit) |
| } |
| if fr.Error != nil { |
| fr.Status = derrors.ToStatus(fr.Error) |
| } |
| if fr.Status == 0 { |
| fr.Status = http.StatusOK |
| } |
| fr.PackageVersionStates = packageVersionStates |
| for _, state := range fr.PackageVersionStates { |
| if state.Status != http.StatusOK { |
| fr.Status = derrors.ToStatus(derrors.HasIncompletePackages) |
| } |
| } |
| return fr |
| } |
| |
| // GetInfo returns the result of a request to the proxy .info endpoint. If |
| // the modulePath is "std", a request to @master will return an empty |
| // commit time. |
| func GetInfo(ctx context.Context, modulePath, requestedVersion string, mg ModuleGetter) (_ *proxy.VersionInfo, err error) { |
| return mg.Info(ctx, modulePath, requestedVersion) |
| } |
| |
| // getGoModPath returns the module path from the go.mod file, as well as the |
| // contents of the file obtained from the module getter. If modulePath is the |
| // standard library, then the contents will be nil. |
| func getGoModPath(ctx context.Context, modulePath, resolvedVersion string, mg ModuleGetter) (string, []byte, error) { |
| goModBytes, err := mg.Mod(ctx, modulePath, resolvedVersion) |
| if err != nil { |
| return "", nil, err |
| } |
| goModPath := modfile.ModulePath(goModBytes) |
| if goModPath == "" { |
| return "", nil, fmt.Errorf("go.mod has no module path: %w", derrors.BadModule) |
| } |
| 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. |
| return goModPath, goModBytes, fmt.Errorf("module path=%s, go.mod path=%s: %w", modulePath, goModPath, derrors.AlternativeModule) |
| } |
| return goModPath, goModBytes, nil |
| } |
| |
| // extractUnitMetas extracts UnitMeta information from the module filesystem and |
| // populates the LazyModule with that information and additional module-level data. |
| func extractUnitMetas(ctx context.Context, minfo internal.ModuleInfo, |
| contentDir fs.FS) (unitMetas []*internal.UnitMeta, _ *godoc.ModuleInfo, _ []*internal.PackageVersionState, err error) { |
| defer derrors.Wrap(&err, "extractUnitMetas(%q, %q)", minfo.ModulePath, minfo.Version) |
| |
| ctx, span := trace.StartSpan(ctx, "fetch.extractUnitMetas") |
| defer span.End() |
| |
| packageMetas, godocModInfo, failedMetaPackages, err := extractPackageMetas(ctx, minfo.ModulePath, minfo.Version, contentDir) |
| if errors.Is(err, ErrModuleContainsNoPackages) { |
| return nil, nil, nil, fmt.Errorf("%v: %w", err.Error(), derrors.BadModule) |
| } |
| if err != nil { |
| return nil, nil, nil, err |
| } |
| return moduleUnitMetas(minfo, packageMetas), godocModInfo, failedMetaPackages, nil |
| } |
| |
| func hasGoModFile(contentDir fs.FS) bool { |
| info, err := fs.Stat(contentDir, "go.mod") |
| return err == nil && !info.IsDir() |
| } |
| |
| // processGoModFile populates mod with information extracted from the contents of the go.mod file. |
| func processGoModFile(goModBytes []byte, mod *internal.ModuleInfo) (err error) { |
| defer derrors.Wrap(&err, "processGoModFile") |
| |
| mf, err := modfile.Parse("go.mod", goModBytes, nil) |
| if err != nil { |
| return err |
| } |
| mod.Deprecated, mod.DeprecationComment = extractDeprecatedComment(mf) |
| return nil |
| } |
| |
| // extractDeprecatedComment looks for "Deprecated" comments in the line comments |
| // before the module declaration. If it finds one, it returns true along with |
| // the text after "Deprecated:". Otherwise it returns false, "". |
| func extractDeprecatedComment(mf *modfile.File) (bool, string) { |
| const prefix = "Deprecated:" |
| |
| if mf.Module == nil { |
| return false, "" |
| } |
| for _, comment := range append(mf.Module.Syntax.Before, mf.Module.Syntax.Suffix...) { |
| text := strings.TrimSpace(strings.TrimPrefix(comment.Token, "//")) |
| if strings.HasPrefix(text, prefix) { |
| return true, strings.TrimSpace(text[len(prefix):]) |
| } |
| } |
| return false, "" |
| } |