diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/api/api.go | 95 | ||||
| -rw-r--r-- | internal/api/api_test.go | 90 |
2 files changed, 154 insertions, 31 deletions
diff --git a/internal/api/api.go b/internal/api/api.go index 87339e19..0f610faf 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "net/http" + "slices" "strconv" "strings" "time" @@ -451,48 +452,82 @@ func trimPath(r *http.Request, prefix string) string { // resolveModulePath determines the correct module path for a given package path and version. // If the module path is not provided, it searches through potential candidate module paths -// derived from the package path. If multiple valid modules contain the package, it returns -// a list of candidates to help the user disambiguate the request. +// derived from the package path. +// +// Resolution logic: +// 1. Use internal.CandidateModulePaths(pkgPath) to get potential candidates (ordered longest first). +// 2. Fetch UnitMeta for each candidate that exists in the data source. +// 3. Check if um.ModulePath == mp (where mp is the candidate module path). If not, ignore it +// (this handles the case where GetUnitMeta falls back to another module when the requested +// module does not exist). +// 4. Filter candidates by eliminating those that are deprecated or retracted. +// 5. If exactly one candidate remains after filtering, return it (HTTP 200). +// 6. If multiple candidates remain, return HTTP 400 with the list of candidates (ambiguity). +// 7. If all candidates are eliminated (e.g., all are deprecated or retracted), fall back to +// the longest matching candidate among those that exist (HTTP 200). func resolveModulePath(r *http.Request, ds internal.DataSource, pkgPath, modulePath, requestedVersion string) (*internal.UnitMeta, error) { if requestedVersion == "" { requestedVersion = version.Latest } - if modulePath == "" { - // Handle potential ambiguity if module is not specified. - candidates := internal.CandidateModulePaths(pkgPath) - var validCandidates []Candidate - var foundUM *internal.UnitMeta - for _, mp := range candidates { - // Check if this module actually exists and contains the package at the requested version. - if m, err := ds.GetUnitMeta(r.Context(), pkgPath, mp, requestedVersion); err == nil { - foundUM = m - validCandidates = append(validCandidates, Candidate{ - ModulePath: mp, - PackagePath: pkgPath, - }) - } else if !errors.Is(err, derrors.NotFound) { - return nil, err - } + if modulePath != "" { + um, err := ds.GetUnitMeta(r.Context(), pkgPath, modulePath, requestedVersion) + if err != nil { + return nil, err } + return um, nil + } - if len(validCandidates) > 1 { - return nil, &Error{ - Code: http.StatusBadRequest, - Message: "ambiguous package path", - Candidates: validCandidates, + candidates := internal.CandidateModulePaths(pkgPath) + var validCandidates []*internal.UnitMeta + for _, mp := range candidates { + if um, err := ds.GetUnitMeta(r.Context(), pkgPath, mp, requestedVersion); err == nil { + // Critical check: ensure the DB actually found the candidate module we requested. + // GetUnitMeta falls back to the best match if the requested module doesn't exist, + // which could lead to false positives (e.g. google.golang.org matching because it + // falls back to google.golang.org/adk/agent). + if um.ModulePath == mp { + validCandidates = append(validCandidates, um) } + } else if !errors.Is(err, derrors.NotFound) { + return nil, err } - if len(validCandidates) == 0 { - return nil, derrors.NotFound + } + + if len(validCandidates) == 0 { + return nil, derrors.NotFound + } + + // Filter candidates based on signals (deprecation, retraction). + goodCandidates := slices.Clone(validCandidates) + goodCandidates = slices.DeleteFunc(goodCandidates, func(um *internal.UnitMeta) bool { + return um.Deprecated || um.Retracted + }) + + switch len(goodCandidates) { + case 1: + return goodCandidates[0], nil + case 0: + // If all candidates are deprecated or retracted, fall back to the longest match. + // Since candidates are ordered longest first, validCandidates[0] is the longest match. + return validCandidates[0], nil + default: + return nil, &Error{ + Code: http.StatusBadRequest, + Message: "ambiguous package path", + Candidates: makeCandidates(goodCandidates), } - return foundUM, nil } +} - um, err := ds.GetUnitMeta(r.Context(), pkgPath, modulePath, requestedVersion) - if err != nil { - return nil, err +func makeCandidates(ums []*internal.UnitMeta) []Candidate { + var r []Candidate + for _, um := range ums { + r = append(r, Candidate{ + ModulePath: um.ModulePath, + PackagePath: um.Path, + }) } - return um, nil + return r } // Values for the Cache-Control header. diff --git a/internal/api/api_test.go b/internal/api/api_test.go index 285005de..64b891c7 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -23,6 +23,22 @@ import ( "golang.org/x/pkgsite/internal/vuln" ) +type fallbackDataSource struct { + internal.DataSource + fallbackMap map[string]string // requested module -> resolved module +} + +func (ds fallbackDataSource) GetUnitMeta(ctx context.Context, path, requestedModulePath, requestedVersion string) (*internal.UnitMeta, error) { + if resolved, ok := ds.fallbackMap[requestedModulePath]; ok { + um, err := ds.DataSource.GetUnitMeta(ctx, path, resolved, requestedVersion) + if err != nil { + return nil, err + } + return um, nil + } + return ds.DataSource.GetUnitMeta(ctx, path, requestedModulePath, requestedVersion) +} + func TestServePackage(t *testing.T) { ctx := context.Background() ds := fakedatasource.New() @@ -141,6 +157,7 @@ func TestServePackage(t *testing.T) { url string wantStatus int want any // Can be *Package or *Error + overrideDS internal.DataSource }{ { name: "basic metadata", @@ -329,12 +346,83 @@ func TestServePackage(t *testing.T) { Imports: []string{pkgPath}, }, }, + { + name: "fallback prevention (false positive candidate)", + url: "/v1/package/example.com/a/b?version=v1.2.3", + wantStatus: http.StatusBadRequest, + want: &Error{ + Code: http.StatusBadRequest, + Message: "ambiguous package path", + Candidates: []Candidate{ + {ModulePath: "example.com/a/b", PackagePath: "example.com/a/b"}, + {ModulePath: "example.com/a", PackagePath: "example.com/a/b"}, + }, + }, + overrideDS: &fallbackDataSource{ + DataSource: ds, + fallbackMap: map[string]string{ + "example.com": "example.com/a/b", // simulate fallback + }, + }, + }, + { + name: "deprecation filtering", + url: "/v1/package/example.com/a/b?version=v1.2.3", + wantStatus: http.StatusOK, + want: &Package{ + Path: pkgPath, + ModulePath: modulePath2, // picked because modulePath1 is deprecated + ModuleVersion: version, + Synopsis: "Synopsis for " + modulePath2, + IsLatest: true, + GOOS: "linux", + GOARCH: "amd64", + }, + overrideDS: func() internal.DataSource { + newDS := fakedatasource.New() + for _, mp := range []string{modulePath1, modulePath2} { + u := &internal.Unit{ + UnitMeta: internal.UnitMeta{ + Path: pkgPath, + ModuleInfo: internal.ModuleInfo{ + ModulePath: mp, + Version: version, + LatestVersion: version, + Deprecated: mp == modulePath1, + }, + Name: "b", + }, + Documentation: []*internal.Documentation{ + { + GOOS: "linux", + GOARCH: "amd64", + Synopsis: "Synopsis for " + mp, + }, + }, + } + newDS.MustInsertModule(ctx, &internal.Module{ + ModuleInfo: internal.ModuleInfo{ + ModulePath: mp, + Version: version, + LatestVersion: version, + Deprecated: mp == modulePath1, + }, + Units: []*internal.Unit{u}, + }) + } + return newDS + }(), + }, } { t.Run(test.name, func(t *testing.T) { r := httptest.NewRequest("GET", test.url, nil) w := httptest.NewRecorder() - if err := ServePackage(w, r, ds); err != nil { + var currentDS internal.DataSource = ds + if test.overrideDS != nil { + currentDS = test.overrideDS + } + if err := ServePackage(w, r, currentDS); err != nil { ServeError(w, r, err) } |
