Skip to content

Commit 84746f0

Browse files
authored
fix(purl): url-encoded slashes in purl (google#5592)
fixes google#5590 When the database worker was migrated from Python to Go, it began using Go's purl.Generate function to generate PURLs. In Go, ecosystems like Packagist, npm, Hex, and SwiftURL were registered to use `simpleGenerator` which did not separate package name into namespace and name components. As a result, the entire package name (e.g. `drupal/colorbox`) was passed as the package name parameter. The underlying Go package-url library correctly URL-encoded the slash, producing `pkg:composer/drupal%2Fcolorbox`.
1 parent 871b91b commit 84746f0

2 files changed

Lines changed: 37 additions & 5 deletions

File tree

go/purl/ecosystems_simple.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package purl
22

33
import (
4+
"strings"
5+
46
"github.com/ossf/osv-schema/bindings/go/osvconstants"
57
"github.com/package-url/packageurl-go"
68
)
@@ -16,6 +18,24 @@ func (g simpleGenerator) generate(_, packageName string) (packageurl.PackageURL,
1618
return *packageurl.NewPackageURL(g.purlType, g.namespace, packageName, "", g.qualifiers, ""), nil
1719
}
1820

21+
// slashGenerator handles PURL mappings where the package name contains a slash-separated namespace.
22+
type slashGenerator struct {
23+
purlType string
24+
qualifiers packageurl.Qualifiers
25+
}
26+
27+
func (g slashGenerator) generate(_, packageName string) (packageurl.PackageURL, error) {
28+
namespace := ""
29+
name := packageName
30+
if strings.Contains(packageName, "/") {
31+
parts := strings.Split(packageName, "/")
32+
name = parts[len(parts)-1]
33+
namespace = strings.Join(parts[:len(parts)-1], "/")
34+
}
35+
36+
return *packageurl.NewPackageURL(g.purlType, namespace, name, "", g.qualifiers, ""), nil
37+
}
38+
1939
// simpleParser handles standard PURL mappings without special logic.
2040
type simpleParser struct {
2141
ecosystem osvconstants.Ecosystem
@@ -36,6 +56,11 @@ func registerSimple(ecosystem osvconstants.Ecosystem, purlType string, namespace
3656
registerParser(purlType, namespace, simpleParser{ecosystem: ecosystem})
3757
}
3858

59+
func registerSlash(ecosystem osvconstants.Ecosystem, purlType string) {
60+
registerGenerator(ecosystem, slashGenerator{purlType: purlType})
61+
registerParser(purlType, "", simpleParser{ecosystem: ecosystem, joinNamespace: true})
62+
}
63+
3964
//nolint:gochecknoinits // init is used here to register simple ecosystems with the global PURL registry.
4065
func init() {
4166
// Language Ecosystems (No namespace)
@@ -44,18 +69,17 @@ func init() {
4469
registerSimple(osvconstants.EcosystemConanCenter, "conan", "", nil)
4570
registerSimple(osvconstants.EcosystemDockerHardenedImages, "dhi", "", nil)
4671
registerSimple(osvconstants.EcosystemHackage, "hackage", "", nil)
47-
registerSimple(osvconstants.EcosystemHex, "hex", "", nil)
72+
registerSlash(osvconstants.EcosystemHex, "hex")
4873
registerSimple(osvconstants.EcosystemJulia, "julia", "", nil)
4974
registerSimple(osvconstants.EcosystemNuGet, "nuget", "", nil)
5075
registerSimple(osvconstants.EcosystemOSSFuzz, "generic", "", nil)
51-
registerSimple(osvconstants.EcosystemPackagist, "composer", "", nil)
76+
registerSlash(osvconstants.EcosystemPackagist, "composer")
5277
registerSimple(osvconstants.EcosystemPub, "pub", "", nil)
5378
registerSimple(osvconstants.EcosystemPyPI, "pypi", "", nil)
5479
registerSimple(osvconstants.EcosystemRubyGems, "gem", "", nil)
55-
registerSimple(osvconstants.EcosystemSwiftURL, "swift", "", nil)
80+
registerSlash(osvconstants.EcosystemSwiftURL, "swift")
5681
registerSimple(osvconstants.EcosystemCratesIO, "cargo", "", nil)
57-
registerGenerator(osvconstants.EcosystemNPM, simpleGenerator{purlType: "npm"})
58-
registerParser("npm", "", simpleParser{ecosystem: osvconstants.EcosystemNPM, joinNamespace: true})
82+
registerSlash(osvconstants.EcosystemNPM, "npm")
5983
registerSimple(osvconstants.EcosystemOpam, "opam", "", nil)
6084

6185
// OS Ecosystems (With static namespace)

go/purl/purl_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ func TestGenerate(t *testing.T) {
2121
{"Go", "github.com/gorilla/mux", "pkg:golang/github.com/gorilla/mux", false},
2222
{"Go", "stdlib", "pkg:golang/stdlib", false},
2323
{"Maven", "org.apache.commons:commons-lang3", "pkg:maven/org.apache.commons/commons-lang3", false},
24+
{"Packagist", "drupal/colorbox", "pkg:composer/drupal/colorbox", false},
25+
{"npm", "@babel/core", "pkg:npm/%40babel/core", false},
26+
{"Hex", "acme/foo", "pkg:hex/acme/foo", false},
27+
{"SwiftURL", "github.com/apple/swift-markdown", "pkg:swift/github.com/apple/swift-markdown", false},
2428
// Error cases
2529
{"UnknownEcosystem", "package", "", true},
2630
}
@@ -58,6 +62,10 @@ func TestParse(t *testing.T) {
5862
{"pkg:golang/stdlib@1.18", "Go", "stdlib", "1.18", false},
5963
{"pkg:maven/org.apache.commons/commons-lang3@3.12.0", "Maven", "org.apache.commons:commons-lang3", "3.12.0", false},
6064
{"pkg:gradle/org.apache.commons/commons-lang3@3.12.0", "Maven", "org.apache.commons:commons-lang3", "3.12.0", false}, // alias
65+
{"pkg:composer/drupal/colorbox@1.2.3", "Packagist", "drupal/colorbox", "1.2.3", false},
66+
{"pkg:npm/%40babel/core@1.2.3", "npm", "@babel/core", "1.2.3", false},
67+
{"pkg:hex/acme/foo@1.2.3", "Hex", "acme/foo", "1.2.3", false},
68+
{"pkg:swift/github.com/apple/swift-markdown@1.2.3", "SwiftURL", "github.com/apple/swift-markdown", "1.2.3", false},
6169
// Error cases
6270
{"invalid-purl", "", "", "", true},
6371
{"pkg:unknown/package@1.0.0", "", "", "", true},

0 commit comments

Comments
 (0)