pkg-proxy/internal/server/resolve.go
Andrew Nesbitt 15c133f1fa
Fix Composer minified metadata expansion and namespaced package routing (#63)
* Fix Composer minified metadata expansion and namespaced package routing

Packagist serves metadata in a minified format where only the first version
entry has all fields and subsequent entries inherit from the previous one.
The proxy was passing this through without expanding it, which meant cooldown
filtering could break the inheritance chain (losing fields like `name`) and
`~dev` sentinel markers were silently dropped.

The proxy now expands the minified format before filtering and rewriting,
ensuring every version entry is self-contained.

Web UI and API routes used single-segment chi URL params for package names,
which broke for Composer's `vendor/name` format. `/package/composer/monolog/monolog`
would match the version show route instead of the package show route.

All `/package/` and related API routes now use wildcard paths with a
`resolvePackageName` helper that tries increasingly longer path prefixes as
package names via DB lookup, correctly handling namespaced packages across
all endpoints (show, version, browse, compare, vulns).

Fixes #61, fixes #62

* Add namespaced package routing tests for all affected ecosystems

Verifies the wildcard routing handles slashes in package names for
npm (@babel/core), Go modules (github.com/stretchr/testify),
OCI images (library/nginx), Conda (conda-forge/numpy), and
Conan (zlib/1.2.13@demo/stable).

* Regenerate swagger docs after route refactor

The swagger annotations for the old per-endpoint handlers were removed
during the wildcard routing refactor. Regenerate to match current state.
2026-04-06 13:07:02 +01:00

41 lines
1.4 KiB
Go

package server
import (
"strings"
"github.com/git-pkgs/proxy/internal/database"
)
// resolvePackageName determines the package name from a wildcard path by
// checking the database. This handles namespaced packages like Composer's
// vendor/name format where the package name contains a slash.
//
// It tries the full path as a package name first. If not found, it splits
// off the last segment as a non-name suffix (version, action, etc.) and
// tries again, working backwards until a match is found or segments run out.
//
// Returns the package name and the remaining path segments after the name.
// If no package is found, returns empty name and the original segments.
func resolvePackageName(db *database.DB, ecosystem string, segments []string) (name string, rest []string) {
// Try increasingly longer prefixes as the package name.
// Start with the longest possible name (all segments) and work down.
for i := len(segments); i >= 1; i-- {
candidate := strings.Join(segments[:i], "/")
pkg, err := db.GetPackageByEcosystemName(ecosystem, candidate)
if err == nil && pkg != nil {
return candidate, segments[i:]
}
}
return "", segments
}
// splitWildcardPath splits a chi wildcard path value into segments,
// trimming any leading/trailing slashes.
func splitWildcardPath(path string) []string {
path = strings.Trim(path, "/")
if path == "" {
return nil
}
return strings.Split(path, "/")
}