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) }