diff --git a/go.mod b/go.mod index 731843ad48ad..c0acdf752b87 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( github.com/opencontainers/image-spec v1.1.0 github.com/opencontainers/runtime-spec v1.2.0 github.com/opencontainers/selinux v1.11.0 - github.com/package-url/packageurl-go v0.1.1-0.20220428063043-89078438f170 + github.com/package-url/packageurl-go v0.1.3 github.com/pelletier/go-toml v1.9.5 github.com/pkg/errors v0.9.1 github.com/pkg/profile v1.7.0 diff --git a/go.sum b/go.sum index a9bc4963b706..89b7b46d1524 100644 --- a/go.sum +++ b/go.sum @@ -306,8 +306,8 @@ github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= -github.com/package-url/packageurl-go v0.1.1-0.20220428063043-89078438f170 h1:DiLBVp4DAcZlBVBEtJpNWZpZVq0AEeCY7Hqk8URVs4o= -github.com/package-url/packageurl-go v0.1.1-0.20220428063043-89078438f170/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c= +github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs= +github.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= diff --git a/vendor/github.com/package-url/packageurl-go/.gitignore b/vendor/github.com/package-url/packageurl-go/.gitignore index a1338d68517e..b5b0dd3f7c34 100644 --- a/vendor/github.com/package-url/packageurl-go/.gitignore +++ b/vendor/github.com/package-url/packageurl-go/.gitignore @@ -12,3 +12,5 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ + +testdata/test-suite-data.json diff --git a/vendor/github.com/package-url/packageurl-go/Makefile b/vendor/github.com/package-url/packageurl-go/Makefile index f6e71425f759..e0b23e4dfe96 100644 --- a/vendor/github.com/package-url/packageurl-go/Makefile +++ b/vendor/github.com/package-url/packageurl-go/Makefile @@ -1,9 +1,12 @@ .PHONY: test clean lint test: - curl -L https://raw.githubusercontent.com/package-url/purl-spec/master/test-suite-data.json -o testdata/test-suite-data.json + curl -Ls https://raw.githubusercontent.com/package-url/purl-spec/master/test-suite-data.json -o testdata/test-suite-data.json go test -v -cover ./... +fuzz: + go test -fuzztime=1m -fuzz . + clean: find . -name "test-suite-data.json" | xargs rm -f diff --git a/vendor/github.com/package-url/packageurl-go/README.md b/vendor/github.com/package-url/packageurl-go/README.md index 783985498b0b..47856e700518 100644 --- a/vendor/github.com/package-url/packageurl-go/README.md +++ b/vendor/github.com/package-url/packageurl-go/README.md @@ -58,17 +58,33 @@ func main() { Testing using the normal ``go test`` command. Using ``make test`` will pull the test fixtures shared between all package-url projects and then execute the tests. ``` -$ make test -curl -L https://raw.githubusercontent.com/package-url/purl-test-suite/master/test-suite-data.json -o testdata/test-suite-data.json - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed -100 7181 100 7181 0 0 1202 0 0:00:05 0:00:05 --:--:-- 1611 +curl -Ls https://raw.githubusercontent.com/package-url/purl-spec/master/test-suite-data.json -o testdata/test-suite-data.json go test -v -cover ./... === RUN TestFromStringExamples --- PASS: TestFromStringExamples (0.00s) === RUN TestToStringExamples --- PASS: TestToStringExamples (0.00s) +=== RUN TestStringer +--- PASS: TestStringer (0.00s) +=== RUN TestQualifiersMapConversion +--- PASS: TestQualifiersMapConversion (0.00s) PASS -coverage: 94.7% of statements -ok github.com/package-url/packageurl-go 0.002s + github.com/package-url/packageurl-go coverage: 90.7% of statements +ok github.com/package-url/packageurl-go 0.004s coverage: 90.7% of statements ``` + +## Fuzzing + +Fuzzing is done with standard [Go fuzzing](https://go.dev/doc/fuzz/), introduced in Go 1.18. + +Fuzz tests check for inputs that cause `FromString` to panic. + +Using `make fuzz` will run fuzz tests for one minute. + +To run fuzz tests longer: + +``` +go test -fuzztime=60m -fuzz . +``` + +Or omit `-fuzztime` entirely to run indefinitely. diff --git a/vendor/github.com/package-url/packageurl-go/packageurl.go b/vendor/github.com/package-url/packageurl-go/packageurl.go index 3cba7095d5f1..0dd89a7883cc 100644 --- a/vendor/github.com/package-url/packageurl-go/packageurl.go +++ b/vendor/github.com/package-url/packageurl-go/packageurl.go @@ -27,6 +27,7 @@ import ( "errors" "fmt" "net/url" + "path" "regexp" "sort" "strings" @@ -39,18 +40,30 @@ var ( // '-' and '_' (period, dash and underscore). // - A key cannot start with a number. QualifierKeyPattern = regexp.MustCompile(`^[A-Za-z\.\-_][0-9A-Za-z\.\-_]*$`) + // TypePattern describes a valid type: + // + // - The type must be composed only of ASCII letters and numbers, '.', + // '+' and '-' (period, plus and dash). + // - A type cannot start with a number. + TypePattern = regexp.MustCompile(`^[A-Za-z\.\-\+][0-9A-Za-z\.\-\+]*$`) ) // These are the known purl types as defined in the spec. Some of these require // special treatment during parsing. // https://github.com/package-url/purl-spec#known-purl-types var ( + // TypeAlpm is a pkg:alpm purl. + TypeAlpm = "alpm" + // TypeApk is a pkg:apk purl. + TypeApk = "apk" // TypeBitbucket is a pkg:bitbucket purl. TypeBitbucket = "bitbucket" - // TypeCocoapods is a pkg:cocoapods purl. - TypeCocoapods = "cocoapods" + // TypeBitnami is a pkg:bitnami purl. + TypeBitnami = "bitnami" // TypeCargo is a pkg:cargo purl. TypeCargo = "cargo" + // TypeCocoapods is a pkg:cocoapods purl. + TypeCocoapods = "cocoapods" // TypeComposer is a pkg:composer purl. TypeComposer = "composer" // TypeConan is a pkg:conan purl. @@ -75,6 +88,10 @@ var ( TypeHackage = "hackage" // TypeHex is a pkg:hex purl. TypeHex = "hex" + // TypeHuggingface is pkg:huggingface purl. + TypeHuggingface = "huggingface" + // TypeMLflow is pkg:mlflow purl. + TypeMLFlow = "mlflow" // TypeMaven is a pkg:maven purl. TypeMaven = "maven" // TypeNPM is a pkg:npm purl. @@ -83,12 +100,156 @@ var ( TypeNuget = "nuget" // TypeOCI is a pkg:oci purl TypeOCI = "oci" + // TypePub is a pkg:pub purl. + TypePub = "pub" // TypePyPi is a pkg:pypi purl. TypePyPi = "pypi" + // TypeQPKG is a pkg:qpkg purl. + TypeQpkg = "qpkg" // TypeRPM is a pkg:rpm purl. TypeRPM = "rpm" + // TypeSWID is pkg:swid purl + TypeSWID = "swid" // TypeSwift is pkg:swift purl TypeSwift = "swift" + + // KnownTypes is a map of types that are officially supported by the spec. + // See https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#known-purl-types + KnownTypes = map[string]struct{}{ + TypeAlpm: {}, + TypeApk: {}, + TypeBitbucket: {}, + TypeBitnami: {}, + TypeCargo: {}, + TypeCocoapods: {}, + TypeComposer: {}, + TypeConan: {}, + TypeConda: {}, + TypeCran: {}, + TypeDebian: {}, + TypeDocker: {}, + TypeGem: {}, + TypeGeneric: {}, + TypeGithub: {}, + TypeGolang: {}, + TypeHackage: {}, + TypeHex: {}, + TypeHuggingface: {}, + TypeMaven: {}, + TypeMLFlow: {}, + TypeNPM: {}, + TypeNuget: {}, + TypeOCI: {}, + TypePub: {}, + TypePyPi: {}, + TypeQpkg: {}, + TypeRPM: {}, + TypeSWID: {}, + TypeSwift: {}, + } + + TypeApache = "apache" + TypeAndroid = "android" + TypeAtom = "atom" + TypeBower = "bower" + TypeBrew = "brew" + TypeBuildroot = "buildroot" + TypeCarthage = "carthage" + TypeChef = "chef" + TypeChocolatey = "chocolatey" + TypeClojars = "clojars" + TypeCoreos = "coreos" + TypeCpan = "cpan" + TypeCtan = "ctan" + TypeCrystal = "crystal" + TypeDrupal = "drupal" + TypeDtype = "dtype" + TypeDub = "dub" + TypeElm = "elm" + TypeEclipse = "eclipse" + TypeGitea = "gitea" + TypeGitlab = "gitlab" + TypeGradle = "gradle" + TypeGuix = "guix" + TypeHaxe = "haxe" + TypeHelm = "helm" + TypeJulia = "julia" + TypeLua = "lua" + TypeMelpa = "melpa" + TypeMeteor = "meteor" + TypeNim = "nim" + TypeNix = "nix" + TypeOpam = "opam" + TypeOpenwrt = "openwrt" + TypeOsgi = "osgi" + TypeP2 = "p2" + TypePear = "pear" + TypePecl = "pecl" + TypePERL6 = "perl6" + TypePlatformio = "platformio" + TypeEbuild = "ebuild" + TypePuppet = "puppet" + TypeSourceforge = "sourceforge" + TypeSublime = "sublime" + TypeTerraform = "terraform" + TypeVagrant = "vagrant" + TypeVim = "vim" + TypeWORDPRESS = "wordpress" + TypeYocto = "yocto" + + // CandidateTypes is a map of types that are not yet officially supported by the spec, + // but are being considered for inclusion. + // See https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#other-candidate-types-to-define + CandidateTypes = map[string]struct{}{ + TypeApache: {}, + TypeAndroid: {}, + TypeAtom: {}, + TypeBower: {}, + TypeBrew: {}, + TypeBuildroot: {}, + TypeCarthage: {}, + TypeChef: {}, + TypeChocolatey: {}, + TypeClojars: {}, + TypeCoreos: {}, + TypeCpan: {}, + TypeCtan: {}, + TypeCrystal: {}, + TypeDrupal: {}, + TypeDtype: {}, + TypeDub: {}, + TypeElm: {}, + TypeEclipse: {}, + TypeGitea: {}, + TypeGitlab: {}, + TypeGradle: {}, + TypeGuix: {}, + TypeHaxe: {}, + TypeHelm: {}, + TypeJulia: {}, + TypeLua: {}, + TypeMelpa: {}, + TypeMeteor: {}, + TypeNim: {}, + TypeNix: {}, + TypeOpam: {}, + TypeOpenwrt: {}, + TypeOsgi: {}, + TypeP2: {}, + TypePear: {}, + TypePecl: {}, + TypePERL6: {}, + TypePlatformio: {}, + TypeEbuild: {}, + TypePuppet: {}, + TypeSourceforge: {}, + TypeSublime: {}, + TypeTerraform: {}, + TypeVagrant: {}, + TypeVim: {}, + TypeWORDPRESS: {}, + TypeYocto: {}, + } ) // Qualifier represents a single key=value qualifier in the package url @@ -106,6 +267,15 @@ func (q Qualifier) String() string { // in the package URL. type Qualifiers []Qualifier +// urlQuery returns a raw URL query with all the qualifiers as keys + values. +func (q Qualifiers) urlQuery() (rawQuery string) { + v := make(url.Values) + for _, qq := range q { + v.Add(qq.Key, qq.Value) + } + return v.Encode() +} + // QualifiersFromMap constructs a Qualifiers slice from a string map. To get a // deterministic qualifier order (despite maps not providing any iteration order // guarantees) the returned Qualifiers are sorted in increasing order of key. @@ -143,6 +313,33 @@ func (qq Qualifiers) String() string { return strings.Join(kvPairs, "&") } +func (qq *Qualifiers) Normalize() error { + qs := *qq + normedQQ := make(Qualifiers, 0, len(qs)) + for _, q := range qs { + if q.Key == "" { + return fmt.Errorf("key is missing from qualifier: %v", q) + } + if q.Value == "" { + // Empty values are equivalent to the key being omitted from the PackageURL. + continue + } + key := strings.ToLower(q.Key) + if !validQualifierKey(key) { + return fmt.Errorf("invalid qualifier key: %q", key) + } + normedQQ = append(normedQQ, Qualifier{key, q.Value}) + } + sort.Slice(normedQQ, func(i, j int) bool { return normedQQ[i].Key < normedQQ[j].Key }) + for i := 1; i < len(normedQQ); i++ { + if normedQQ[i-1].Key == normedQQ[i].Key { + return fmt.Errorf("duplicate qualifier key: %q", normedQQ[i].Key) + } + } + *qq = normedQQ + return nil +} + // PackageURL is the struct representation of the parts that make a package url type PackageURL struct { Type string @@ -170,39 +367,30 @@ func NewPackageURL(purlType, namespace, name, version string, // ToString returns the human-readable instance of the PackageURL structure. // This is the literal purl as defined by the spec. func (p *PackageURL) ToString() string { - // Start with the type and a colon - purl := fmt.Sprintf("pkg:%s/", p.Type) - // Add namespaces if provided - if p.Namespace != "" { - var ns []string - for _, item := range strings.Split(p.Namespace, "/") { - ns = append(ns, url.QueryEscape(item)) + u := &url.URL{ + Scheme: "pkg", + RawQuery: p.Qualifiers.urlQuery(), + Fragment: p.Subpath, + } + + paths := []string{p.Type} + // we need to escape each segment by itself, so that we don't escape "/" in the namespace. + for _, segment := range strings.Split(p.Namespace, "/") { + if segment == "" { + continue } - purl = purl + strings.Join(ns, "/") + "/" + paths = append(paths, escape(segment)) } - // The name is always required and must be a percent-encoded string - // Use url.QueryEscape instead of PathEscape, as it handles @ signs - purl = purl + url.QueryEscape(p.Name) - // If a version is provided, add it after the at symbol + + nameWithVersion := escape(p.Name) if p.Version != "" { - // A name must be a percent-encoded string - purl = purl + "@" + url.PathEscape(p.Version) + nameWithVersion += "@" + escape(p.Version) } - // Iterate over qualifiers and make groups of key=value - var qualifiers []string - for _, q := range p.Qualifiers { - qualifiers = append(qualifiers, q.String()) - } - // If there are one or more key=value pairs, append on the package url - if len(qualifiers) != 0 { - purl = purl + "?" + strings.Join(qualifiers, "&") - } - // Add a subpath if available - if p.Subpath != "" { - purl = purl + "#" + p.Subpath - } - return purl + paths = append(paths, nameWithVersion) + + u.Opaque = strings.Join(paths, "/") + return u.String() } func (p PackageURL) String() string { @@ -211,138 +399,176 @@ func (p PackageURL) String() string { // FromString parses a valid package url string into a PackageURL structure func FromString(purl string) (PackageURL, error) { - initialIndex := strings.Index(purl, "#") - // Start with purl being stored in the remainder - remainder := purl - substring := "" - if initialIndex != -1 { - initialSplit := strings.SplitN(purl, "#", 2) - remainder = initialSplit[0] - rightSide := initialSplit[1] - rightSide = strings.TrimLeft(rightSide, "/") - rightSide = strings.TrimRight(rightSide, "/") - var rightSides []string - - for _, item := range strings.Split(rightSide, "/") { - item = strings.Replace(item, ".", "", -1) - item = strings.Replace(item, "..", "", -1) - if item != "" { - i, err := url.PathUnescape(item) - if err != nil { - return PackageURL{}, fmt.Errorf("failed to unescape path: %s", err) - } - rightSides = append(rightSides, i) - } - } - substring = strings.Join(rightSides, "/") - } - qualifiers := Qualifiers{} - index := strings.LastIndex(remainder, "?") - // If we don't have anything to split then return an empty result - if index != -1 { - qualifier := remainder[index+1:] - for _, item := range strings.Split(qualifier, "&") { - kv := strings.Split(item, "=") - key := strings.ToLower(kv[0]) - key, err := url.PathUnescape(key) - if err != nil { - return PackageURL{}, fmt.Errorf("failed to unescape qualifier key: %s", err) - } - if !validQualifierKey(key) { - return PackageURL{}, fmt.Errorf("invalid qualifier key: '%s'", key) - } - // TODO - // - If the `key` is `checksums`, split the `value` on ',' to create - // a list of `checksums` - if kv[1] == "" { - continue - } - value, err := url.PathUnescape(kv[1]) - if err != nil { - return PackageURL{}, fmt.Errorf("failed to unescape qualifier value: %s", err) - } - qualifiers = append(qualifiers, Qualifier{key, value}) - } - remainder = remainder[:index] + u, err := url.Parse(purl) + if err != nil { + return PackageURL{}, fmt.Errorf("failed to parse as URL: %w", err) } - nextSplit := strings.SplitN(remainder, ":", 2) - if len(nextSplit) != 2 || nextSplit[0] != "pkg" { - return PackageURL{}, errors.New("scheme is missing") + if u.Scheme != "pkg" { + return PackageURL{}, fmt.Errorf("purl scheme is not \"pkg\": %q", u.Scheme) } - // leading slashes after pkg: are to be ignored (pkg://maven is - // equivalent to pkg:maven) - remainder = strings.TrimLeft(nextSplit[1], "/") - nextSplit = strings.SplitN(remainder, "/", 2) - if len(nextSplit) != 2 { - return PackageURL{}, errors.New("type is missing") + p := u.Opaque + // if a purl starts with pkg:/ or even pkg://, we need to fall back to host + path. + if p == "" { + p = strings.TrimPrefix(path.Join(u.Host, u.Path), "/") } - // purl type is case-insensitive, canonical form is lower-case - purlType := strings.ToLower(nextSplit[0]) - remainder = nextSplit[1] - index = strings.LastIndex(remainder, "/") - name := typeAdjustName(purlType, remainder[index+1:]) - version := "" + typ, p, ok := strings.Cut(p, "/") + if !ok { + return PackageURL{}, fmt.Errorf("purl is missing type or name") + } + typ = strings.ToLower(typ) - atIndex := strings.Index(name, "@") - if atIndex != -1 { - v, err := url.PathUnescape(name[atIndex+1:]) - if err != nil { - return PackageURL{}, fmt.Errorf("failed to unescape purl version: %s", err) + qualifiers, err := parseQualifiers(u.RawQuery) + if err != nil { + return PackageURL{}, fmt.Errorf("invalid qualifiers: %w", err) + } + namespace, name, version, err := separateNamespaceNameVersion(p) + if err != nil { + return PackageURL{}, err + } + + pURL := PackageURL{ + Qualifiers: qualifiers, + Type: typ, + Namespace: namespace, + Name: name, + Version: version, + Subpath: u.Fragment, + } + + err = pURL.Normalize() + return pURL, err +} + +// Normalize converts p to its canonical form, returning an error if p is invalid. +func (p *PackageURL) Normalize() error { + typ := strings.ToLower(p.Type) + if !validType(typ) { + return fmt.Errorf("invalid type %q", typ) + } + namespace := strings.Trim(p.Namespace, "/") + if err := p.Qualifiers.Normalize(); err != nil { + return fmt.Errorf("invalid qualifiers: %v", err) + } + if p.Name == "" { + return errors.New("purl is missing name") + } + subpath := strings.Trim(p.Subpath, "/") + segs := strings.Split(p.Subpath, "/") + for i, s := range segs { + if (s == "." || s == "..") && i != 0 { + return fmt.Errorf("invalid Package URL subpath: %q", p.Subpath) } - version = v + } + *p = PackageURL{ + Type: typ, + Namespace: typeAdjustNamespace(typ, namespace), + Name: typeAdjustName(typ, p.Name, p.Qualifiers), + Version: typeAdjustVersion(typ, p.Version), + Qualifiers: p.Qualifiers, + Subpath: subpath, + } + return validCustomRules(*p) +} + +// escape the given string in a purl-compatible way. +func escape(s string) string { + // for compatibility with other implementations and the purl-spec, we want to escape all + // characters, which is what "QueryEscape" does. The issue with QueryEscape is that it encodes + // " " (space) as "+", which is valid in a query, but invalid in a path (see + // https://stackoverflow.com/questions/2678551/when-should-space-be-encoded-to-plus-or-20) for + // context). + // To work around that, we replace the "+" signs with the path-compatible "%20". + return strings.ReplaceAll(url.QueryEscape(s), "+", "%20") +} - unecapeName, err := url.PathUnescape(name[:atIndex]) +func separateNamespaceNameVersion(path string) (ns, name, version string, err error) { + name = path + + if namespaceSep := strings.LastIndex(name, "/"); namespaceSep != -1 { + ns, name = name[:namespaceSep], name[namespaceSep+1:] + + ns, err = url.PathUnescape(ns) if err != nil { - return PackageURL{}, fmt.Errorf("failed to unescape purl name: %s", err) + return "", "", "", fmt.Errorf("error unescaping namespace: %w", err) } - name = unecapeName } - var namespaces []string - if index != -1 { - remainder = remainder[:index] + if versionSep := strings.LastIndex(name, "@"); versionSep != -1 { + name, version = name[:versionSep], name[versionSep+1:] - for _, item := range strings.Split(remainder, "/") { - if item != "" { - unescaped, err := url.PathUnescape(item) - if err != nil { - return PackageURL{}, fmt.Errorf("failed to unescape path: %s", err) - } - namespaces = append(namespaces, unescaped) - } + version, err = url.PathUnescape(version) + if err != nil { + return "", "", "", fmt.Errorf("error unescaping version: %w", err) } } - namespace := strings.Join(namespaces, "/") - namespace = typeAdjustNamespace(purlType, namespace) - // Fail if name is empty at this point - if name == "" { - return PackageURL{}, errors.New("name is required") + name, err = url.PathUnescape(name) + if err != nil { + return "", "", "", fmt.Errorf("error unescaping name: %w", err) } - err := validCustomRules(purlType, name, namespace, version, qualifiers) - if err != nil { - return PackageURL{}, err + if name == "" { + return "", "", "", fmt.Errorf("purl is missing name") } - return PackageURL{ - Type: purlType, - Namespace: namespace, - Name: name, - Version: version, - Qualifiers: qualifiers, - Subpath: substring, - }, nil + return ns, name, version, nil +} + +func parseQualifiers(rawQuery string) (Qualifiers, error) { + // we need to parse the qualifiers ourselves and cannot rely on the `url.Query` type because + // that uses a map, meaning it's unordered. We want to keep the order of the qualifiers, so this + // function re-implements the `url.parseQuery` function based on our `Qualifier` type. Most of + // the code here is taken from `url.parseQuery`. + q := Qualifiers{} + for rawQuery != "" { + var key string + key, rawQuery, _ = strings.Cut(rawQuery, "&") + if strings.Contains(key, ";") { + return nil, fmt.Errorf("invalid semicolon separator in query") + } + if key == "" { + continue + } + key, value, _ := strings.Cut(key, "=") + key, err := url.QueryUnescape(key) + if err != nil { + return nil, fmt.Errorf("error unescaping qualifier key %q", key) + } + + if !validQualifierKey(key) { + return nil, fmt.Errorf("invalid qualifier key: '%s'", key) + } + + value, err = url.QueryUnescape(value) + if err != nil { + return nil, fmt.Errorf("error unescaping qualifier value %q", value) + } + + q = append(q, Qualifier{ + Key: strings.ToLower(key), + Value: value, + }) + } + return q, nil } // Make any purl type-specific adjustments to the parsed namespace. // See https://github.com/package-url/purl-spec#known-purl-types func typeAdjustNamespace(purlType, ns string) string { switch purlType { - case TypeBitbucket, TypeDebian, TypeGithub, TypeGolang, TypeNPM, TypeRPM: + case TypeAlpm, + TypeApk, + TypeBitbucket, + TypeComposer, + TypeDebian, + TypeGithub, + TypeGolang, + TypeNPM, + TypeRPM, + TypeQpkg: return strings.ToLower(ns) } return ns @@ -350,28 +576,73 @@ func typeAdjustNamespace(purlType, ns string) string { // Make any purl type-specific adjustments to the parsed name. // See https://github.com/package-url/purl-spec#known-purl-types -func typeAdjustName(purlType, name string) string { +func typeAdjustName(purlType, name string, qualifiers Qualifiers) string { + quals := qualifiers.Map() switch purlType { - case TypeBitbucket, TypeDebian, TypeGithub, TypeGolang, TypeNPM: + case TypeAlpm, + TypeApk, + TypeBitbucket, + TypeBitnami, + TypeComposer, + TypeDebian, + TypeGithub, + TypeGolang, + TypeNPM: return strings.ToLower(name) case TypePyPi: return strings.ToLower(strings.ReplaceAll(name, "_", "-")) + case TypeMLFlow: + return adjustMlflowName(name, quals) } return name } +// Make any purl type-specific adjustments to the parsed version. +// See https://github.com/package-url/purl-spec#known-purl-types +func typeAdjustVersion(purlType, version string) string { + switch purlType { + case TypeHuggingface: + return strings.ToLower(version) + } + return version +} + +// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#mlflow +func adjustMlflowName(name string, qualifiers map[string]string) string { + if repo, ok := qualifiers["repository_url"]; ok { + if strings.Contains(repo, "azureml") { + // Azure ML is case-sensitive and must be kept as-is + return name + } else if strings.Contains(repo, "databricks") { + // Databricks is case-insensitive and must be lowercased + return strings.ToLower(name) + } else { + // Unknown repository type, keep as-is + return name + } + } else { + // No repository qualifier given, keep as-is + return name + } +} + // validQualifierKey validates a qualifierKey against our QualifierKeyPattern. func validQualifierKey(key string) bool { return QualifierKeyPattern.MatchString(key) } +// validType validates a type against our TypePattern. +func validType(typ string) bool { + return TypePattern.MatchString(typ) +} + // validCustomRules evaluates additional rules for each package url type, as specified in the package-url specification. // On success, it returns nil. On failure, a descriptive error will be returned. -func validCustomRules(purlType, name, ns, version string, qualifiers Qualifiers) error { - q := qualifiers.Map() - switch purlType { +func validCustomRules(p PackageURL) error { + q := p.Qualifiers.Map() + switch p.Type { case TypeConan: - if ns != "" { + if p.Namespace != "" { if val, ok := q["channel"]; ok { if val == "" { return errors.New("the qualifier channel must be not empty if namespace is present") @@ -387,14 +658,14 @@ func validCustomRules(purlType, name, ns, version string, qualifiers Qualifiers) } } case TypeSwift: - if ns == "" { + if p.Namespace == "" { return errors.New("namespace is required") } - if version == "" { + if p.Version == "" { return errors.New("version is required") } case TypeCran: - if version == "" { + if p.Version == "" { return errors.New("version is required") } } diff --git a/vendor/modules.txt b/vendor/modules.txt index 179c0a412b4a..a68396d90456 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -678,8 +678,8 @@ github.com/opencontainers/runtime-spec/specs-go/features github.com/opencontainers/selinux/go-selinux github.com/opencontainers/selinux/go-selinux/label github.com/opencontainers/selinux/pkg/pwalkdir -# github.com/package-url/packageurl-go v0.1.1-0.20220428063043-89078438f170 -## explicit; go 1.17 +# github.com/package-url/packageurl-go v0.1.3 +## explicit; go 1.18 github.com/package-url/packageurl-go # github.com/pelletier/go-toml v1.9.5 ## explicit; go 1.12