diff --git a/components/producers/blackduck/main.go b/components/producers/blackduck/main.go index e08585f71..b3fec971a 100644 --- a/components/producers/blackduck/main.go +++ b/components/producers/blackduck/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "fmt" "log" "strconv" @@ -24,7 +25,7 @@ func main() { } var results BlackduckOut - if err := producers.ParseJSON(inFile, &results); err != nil { + if err := json.Unmarshal(inFile, &results); err != nil { log.Fatal(err) } diff --git a/vendor/github.com/package-url/packageurl-go/.gitignore b/vendor/github.com/package-url/packageurl-go/.gitignore index d373807a9..b5b0dd3f7 100644 --- a/vendor/github.com/package-url/packageurl-go/.gitignore +++ b/vendor/github.com/package-url/packageurl-go/.gitignore @@ -7,10 +7,10 @@ # Test binary, build with `go test -c` *.test -testdata/*json - # Output of the go coverage tool, specifically when used with LiteIDE *.out # 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/.golangci.yaml b/vendor/github.com/package-url/packageurl-go/.golangci.yaml new file mode 100644 index 000000000..73a5741c9 --- /dev/null +++ b/vendor/github.com/package-url/packageurl-go/.golangci.yaml @@ -0,0 +1,17 @@ +# individual linter configs go here +linters-settings: + +# default linters are enabled `golangci-lint help linters` +linters: + disable-all: true + enable: + - deadcode + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - structcheck + - typecheck + - unused + - varcheck \ No newline at end of file diff --git a/vendor/github.com/package-url/packageurl-go/.travis.yml b/vendor/github.com/package-url/packageurl-go/.travis.yml deleted file mode 100644 index 1bb07d03a..000000000 --- a/vendor/github.com/package-url/packageurl-go/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: go - -go: - - 1.12 - - tip - -install: true - -matrix: - allow_failures: - - go: tip - fast_finish: true - -script: - - make lint - - make test - -notifications: - email: false diff --git a/vendor/github.com/package-url/packageurl-go/mit.LICENSE b/vendor/github.com/package-url/packageurl-go/LICENSE similarity index 100% rename from vendor/github.com/package-url/packageurl-go/mit.LICENSE rename to vendor/github.com/package-url/packageurl-go/LICENSE diff --git a/vendor/github.com/package-url/packageurl-go/Makefile b/vendor/github.com/package-url/packageurl-go/Makefile index f6e71425f..e0b23e4df 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 68b42ac18..47856e700 100644 --- a/vendor/github.com/package-url/packageurl-go/README.md +++ b/vendor/github.com/package-url/packageurl-go/README.md @@ -1,8 +1,8 @@ # packageurl-go -Go implementation of the package url spec +[![build](https://github.com/package-url/packageurl-go/workflows/test/badge.svg)](https://github.com/package-url/packageurl-go/actions?query=workflow%3Atest) [![Coverage Status](https://coveralls.io/repos/github/package-url/packageurl-go/badge.svg)](https://coveralls.io/github/package-url/packageurl-go) [![PkgGoDev](https://pkg.go.dev/badge/github.com/package-url/packageurl-go)](https://pkg.go.dev/github.com/package-url/packageurl-go) [![Go Report Card](https://goreportcard.com/badge/github.com/package-url/packageurl-go)](https://goreportcard.com/report/github.com/package-url/packageurl-go) -[![Build Status](https://travis-ci.com/package-url/packageurl-go.svg)](https://travis-ci.com/package-url/packageurl-go) +Go implementation of the package url spec. ## Install @@ -55,20 +55,36 @@ func main() { ## Test -Testing using the normal ``go test`` command. Using ``make test`` will pull down the test fixtures shared between all package-url projects and then execute the tests. +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 b521429f5..d200ab5fc 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,40 @@ 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" + // 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. + TypeConan = "conan" + // TypeConda is a pkg:conda purl. + TypeConda = "conda" + // TypeCran is a pkg:cran purl. + TypeCran = "cran" // TypeDebian is a pkg:deb purl. - TypeDebian = "debian" + TypeDebian = "deb" // TypeDocker is a pkg:docker purl. TypeDocker = "docker" // TypeGem is a pkg:gem purl. @@ -61,16 +84,172 @@ var ( TypeGithub = "github" // TypeGolang is a pkg:golang purl. TypeGolang = "golang" + // TypeHackage is a pkg:hackage purl. + 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. TypeNPM = "npm" // TypeNuget is a pkg:nuget purl. 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 @@ -80,7 +259,7 @@ type Qualifier struct { } func (q Qualifier) String() string { - // A value must be must be a percent-encoded string + // A value must be a percent-encoded string return fmt.Sprintf("%s=%s", q.Key, url.PathEscape(q.Value)) } @@ -88,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. @@ -106,7 +294,7 @@ func QualifiersFromMap(mm map[string]string) Qualifiers { // Map converts a Qualifiers struct to a string map. func (qq Qualifiers) Map() map[string]string { - m := make(map[string]string, 0) + m := make(map[string]string) for i := 0; i < len(qq); i++ { k := qq[i].Key @@ -125,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 @@ -149,171 +364,211 @@ func NewPackageURL(purlType, namespace, name, version string, } } -// ToString returns the human readable instance of the PackageURL structure. +// 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 != "" { - 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 - purl = purl + url.PathEscape(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 one or more key=value pairs then 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 { +func (p PackageURL) String() string { return p.ToString() } // 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) + + 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 _, s := range segs { + if s == "." || s == ".." { + return fmt.Errorf("invalid Package URL subpath: %q", p.Subpath) + } + } + *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) +} - atIndex := strings.Index(name, "@") - if atIndex != -1 { - v, err := url.PathUnescape(name[atIndex+1:]) +// 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") +} + +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 version: %s", err) + return "", "", "", fmt.Errorf("error unescaping namespace: %w", err) } - version = v - name = name[:atIndex] } - 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 + name, err = url.PathUnescape(name) + if err != nil { + return "", "", "", fmt.Errorf("error unescaping name: %w", err) + } + if name == "" { - return PackageURL{}, errors.New("name is required") + 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 @@ -321,16 +576,98 @@ 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(p PackageURL) error { + q := p.Qualifiers.Map() + switch p.Type { + case TypeConan: + 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") + } + } else { + return errors.New("channel qualifier does not exist") + } + } else { + if val, ok := q["channel"]; ok { + if val != "" { + return errors.New("namespace is required if channel is non empty") + } + } + } + case TypeSwift: + if p.Namespace == "" { + return errors.New("namespace is required") + } + if p.Version == "" { + return errors.New("version is required") + } + case TypeCran: + if p.Version == "" { + return errors.New("version is required") + } + } + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 5bfe459c3..3d57ba235 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -449,8 +449,8 @@ github.com/opencontainers/go-digest # github.com/owenrumney/go-sarif/v2 v2.1.2 ## explicit; go 1.16 github.com/owenrumney/go-sarif/v2/sarif -# github.com/package-url/packageurl-go v0.1.0 -## explicit; go 1.12 +# github.com/package-url/packageurl-go v0.1.2 +## explicit; go 1.17 github.com/package-url/packageurl-go # github.com/pelletier/go-toml v1.9.4 ## explicit; go 1.12