From dd78cab389a6a50e15118ac42da8340b401788fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramon=20R=C3=BCttimann?= Date: Tue, 18 Jul 2023 16:50:26 +0200 Subject: [PATCH 1/3] fix: escape everything with modified QueryEscape This commit switches to using `QueryEscape` for escaping all components. However, because `QueryEscape` escapes ` ` (space) to `+`, we actually change that to a `%20`, that is the percent-encoded equivalent. `QueryEscape` was built for HTTP Query parameters, and although there is [some discussion](https://stackoverflow.com/questions/2678551/when-should-space-be-encoded-to-plus-or-20) around it, escaping ` ` to a `+` is completely valid. Sadly, other languages like Javascript don't handle that properly, so if we simply used `QueryEscape`, the purl couldn't be parsed by other implementations. By using the universally supported `%20` instead, we restore compatibility. --- packageurl.go | 32 ++++++++++++++++++++++++-------- packageurl_test.go | 6 +++--- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/packageurl.go b/packageurl.go index dc86d25..0796a2d 100644 --- a/packageurl.go +++ b/packageurl.go @@ -200,18 +200,23 @@ func (p *PackageURL) ToString() string { Fragment: p.Subpath, } - nameWithVersion := url.PathEscape(p.Name) + 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 + } + paths = append(paths, escape(segment)) + } + + nameWithVersion := escape(p.Name) if p.Version != "" { - nameWithVersion += "@" + p.Version + nameWithVersion += "@" + escape(p.Version) } - // we use JoinPath and EscapedPath as the behavior for "/" is only correct with that. - // We don't want to escape "/", but want to escape all other characters that are necessary. - u = u.JoinPath(p.Type, p.Namespace, nameWithVersion) - // write the actual path into the "Opaque" block, so that the generated string at the end is - // pkg: and not pkg://. - u.Opaque, u.Path = u.EscapedPath(), "" + paths = append(paths, nameWithVersion) + u.Opaque = strings.Join(paths, "/") return u.String() } @@ -263,6 +268,17 @@ func FromString(purl string) (PackageURL, error) { return pURL, validCustomRules(pURL) } +// 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 diff --git a/packageurl_test.go b/packageurl_test.go index 2aac09c..4423c72 100644 --- a/packageurl_test.go +++ b/packageurl_test.go @@ -302,12 +302,12 @@ func TestQualifiersMapConversion(t *testing.T) { func TestNameEscaping(t *testing.T) { testCases := map[string]string{ - "abc": "pkg:abc", - "ab/c": "pkg:ab%2Fc", + "abc": "pkg:deb/abc", + "ab/c": "pkg:deb/ab%2Fc", } for name, output := range testCases { t.Run(name, func(t *testing.T) { - p := &packageurl.PackageURL{Name: name} + p := &packageurl.PackageURL{Type: "deb", Name: name} if s := p.ToString(); s != output { t.Fatalf("wrong escape. expected=%q, got=%q", output, s) } From 90934ff596b8fa59b84108b541307680cde393e1 Mon Sep 17 00:00:00 2001 From: Antonio Gamez Diaz Date: Mon, 31 Jul 2023 12:27:45 +0200 Subject: [PATCH 2/3] Sort types alphabetically Signed-off-by: Antonio Gamez Diaz --- packageurl.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packageurl.go b/packageurl.go index 0796a2d..10e8c32 100644 --- a/packageurl.go +++ b/packageurl.go @@ -52,10 +52,10 @@ var ( TypeApk = "apk" // TypeBitbucket is a pkg:bitbucket purl. TypeBitbucket = "bitbucket" - // TypeCocoapods is a pkg:cocoapods purl. - TypeCocoapods = "cocoapods" // 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. @@ -80,30 +80,30 @@ var ( TypeHackage = "hackage" // TypeHex is a pkg:hex purl. TypeHex = "hex" + // TypeHuggingface is pkg:huggingface purl. + TypeHuggingface = "huggingface" + // TypeJulia is a pkg:julia purl + TypeJulia = "julia" + // 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" - // TypeQPKG is a pkg:qpkg purl. - TypeQpkg = "qpkg" // TypeOCI is a pkg:oci purl TypeOCI = "oci" // 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" - // TypeHuggingface is pkg:huggingface purl. - TypeHuggingface = "huggingface" - // TypeMLflow is pkg:mlflow purl. - TypeMLFlow = "mlflow" - // TypeJulia is a pkg:julia purl - TypeJulia = "julia" ) // Qualifier represents a single key=value qualifier in the package url From f8bb31c1f10b5f6013a040d46d207a69e93b5918 Mon Sep 17 00:00:00 2001 From: Antonio Gamez Diaz Date: Mon, 31 Jul 2023 12:35:45 +0200 Subject: [PATCH 3/3] Add new purl types `pub` and `bitnami` Signed-off-by: Antonio Gamez Diaz --- packageurl.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packageurl.go b/packageurl.go index 10e8c32..ebef512 100644 --- a/packageurl.go +++ b/packageurl.go @@ -52,6 +52,8 @@ var ( 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. @@ -94,6 +96,8 @@ 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. @@ -379,6 +383,7 @@ func typeAdjustName(purlType, name string, qualifiers Qualifiers) string { case TypeAlpm, TypeApk, TypeBitbucket, + TypeBitnami, TypeComposer, TypeDebian, TypeGithub,