From 0ff8c4debeebebe0b78f8d1487b97aaf992f1445 Mon Sep 17 00:00:00 2001 From: Simon Chrzanowski Date: Wed, 21 Oct 2026 07:02:54 +0200 Subject: [PATCH 1/2] Add bsd PURL type for FreeBSD packages Add the bsd type proposed in package-url/purl-spec#496 and map the freebsd ecosystem to pkg:bsd/freebsd/..., the same way alpine maps to pkg:apk/alpine/.... The type is listed as a local allowance until the spec PR is merged. --- README.md | 2 +- ecosystem.go | 8 ++++++-- ecosystem_test.go | 26 +++++++++++++++++++++++++- spec_test.go | 1 + types.json | 9 +++++++++ 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 578e928..4110954 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ fmt.Println(version) // 1.2.3 Type information is embedded in `types.json`. Type names and namespace requirements are checked against the pinned [purl-spec](https://github.com/package-url/purl-spec) submodule. Default registries and URL templates are maintained locally. -The local types `bioconductor`, `clojars`, `deno`, and `elm` are retained. The legacy `homebrew` type keeps its existing PURLs and registry behavior; use `brew` explicitly for the registered type. Recognition of a type does not imply full support for its normalization or validation rules. +The local types `bioconductor`, `clojars`, `deno`, and `elm` are retained. `bsd` follows the FreeBSD type proposed in [purl-spec#496](https://github.com/package-url/purl-spec/pull/496); the `freebsd` ecosystem maps to `pkg:bsd/freebsd/...`. The legacy `homebrew` type keeps its existing PURLs and registry behavior; use `brew` explicitly for the registered type. Recognition of a type does not imply full support for its normalization or validation rules. ```go purl.KnownTypes() // []string of all known PURL types diff --git a/ecosystem.go b/ecosystem.go index 600805b..1251924 100644 --- a/ecosystem.go +++ b/ecosystem.go @@ -8,6 +8,7 @@ const ( ecosystemAlpine = "alpine" ecosystemArch = "arch" ecosystemComposer = "composer" + ecosystemFreeBSD = "freebsd" ecosystemGit = "git" ecosystemGitHubActions = "github-actions" ecosystemGolang = "golang" @@ -26,6 +27,7 @@ const ( var purlTypeForEcosystem = map[string]string{ ecosystemAlpine: "apk", ecosystemArch: "alpm", + ecosystemFreeBSD: "bsd", ecosystemRubyGems: purlTypeGem, ecosystemPackagist: ecosystemComposer, ecosystemGitHubActions: purlTypeGitHubActions, @@ -72,8 +74,9 @@ var depsdevSystemNames = map[string]string{ // defaultNamespaces defines default namespaces for certain ecosystems. var defaultNamespaces = map[string]string{ - ecosystemAlpine: ecosystemAlpine, - ecosystemArch: ecosystemArch, + ecosystemAlpine: ecosystemAlpine, + ecosystemArch: ecosystemArch, + ecosystemFreeBSD: ecosystemFreeBSD, } // NormalizeEcosystem returns the canonical ecosystem name. @@ -149,6 +152,7 @@ func PURLTypeToDepsdev(purlType string) string { // - composer: vendor/package -> namespace="vendor", name="package" // - alpine: pkg -> namespace="alpine", name="pkg" // - arch: pkg -> namespace="arch", name="pkg" +// - freebsd: pkg -> namespace="freebsd", name="pkg" // - swift: host/owner/package -> namespace="host/owner", name="package" // // Swift registry identities do not contain source repository coordinates and diff --git a/ecosystem_test.go b/ecosystem_test.go index 52bd4e1..7775754 100644 --- a/ecosystem_test.go +++ b/ecosystem_test.go @@ -39,6 +39,7 @@ func TestEcosystemToPURLType(t *testing.T) { {"npm", "npm"}, {"alpine", "apk"}, {"arch", "alpm"}, + {"freebsd", "bsd"}, {"rubygems", "gem"}, {"packagist", "composer"}, {"cargo", "cargo"}, @@ -65,6 +66,7 @@ func TestPURLTypeToEcosystem(t *testing.T) { }{ {"apk", "alpine"}, {"alpm", "arch"}, + {"bsd", "freebsd"}, {"gem", "rubygems"}, {"composer", "packagist"}, {"githubactions", "github-actions"}, @@ -210,6 +212,7 @@ func TestMakePURLString(t *testing.T) { {"alpine default namespace", "alpine", "curl", "8.0.0", "pkg:apk/alpine/curl@8.0.0"}, {"alpine default namespace escaping", "alpine", "curl/ssl", "8.0.0-r0", "pkg:apk/alpine/curl%2Fssl@8.0.0-r0"}, {"arch default namespace", "arch", "base", "1.0", "pkg:alpm/arch/base@1.0"}, + {"freebsd default namespace", "freebsd", "py311-requests", "2.32.3_1", "pkg:bsd/freebsd/py311-requests@2.32.3_1"}, {"github-actions", "github-actions", "actions/checkout", "v4", "pkg:githubactions/actions/checkout@v4"}, {"github-actions path", "github-actions", "actions/cache/restore", "v4", "pkg:githubactions/actions/cache@v4"}, {"cargo", "cargo", "serde", "1.0.0", "pkg:cargo/serde@1.0.0"}, @@ -280,7 +283,7 @@ func TestSupportedEcosystems(t *testing.T) { has[e] = true } - required := []string{"npm", "cargo", "pypi", "maven", "golang", "go", "gem", "alpine"} + required := []string{"npm", "cargo", "pypi", "maven", "golang", "go", "gem", "alpine", "freebsd"} for _, r := range required { if !has[r] { t.Errorf("SupportedEcosystems() missing %q", r) @@ -301,6 +304,7 @@ func TestIsValidEcosystem(t *testing.T) { {"gem", true}, {"rubygems", true}, {"alpine", true}, + {"freebsd", true}, {"notarealecosystem", false}, } @@ -312,3 +316,23 @@ func TestIsValidEcosystem(t *testing.T) { }) } } + +func TestFreeBSDPURL(t *testing.T) { + const s = "pkg:bsd/freebsd/emacs@30.1_2?distro=14.3&epoch=3" + p, err := Parse(s) + if err != nil { + t.Fatal(err) + } + if p.Type != "bsd" || p.Namespace != "freebsd" || p.Name != "emacs" || p.Version != "30.1_2" { + t.Errorf("Parse(%q) = %+v", s, p.PackageURL) + } + if got := p.String(); got != s { + t.Errorf("String() = %q, want %q", got, s) + } + if got := PURLTypeToEcosystem(p.Type); got != "freebsd" { + t.Errorf("PURLTypeToEcosystem(%q) = %q, want freebsd", p.Type, got) + } + if cfg := TypeInfo("bsd"); cfg == nil || !cfg.NamespaceRequired() { + t.Error("bsd type must require a namespace") + } +} diff --git a/spec_test.go b/spec_test.go index cb39657..75cd1be 100644 --- a/spec_test.go +++ b/spec_test.go @@ -28,6 +28,7 @@ func TestSpecTypeMetadata(t *testing.T) { localTypes := map[string]string{ "bioconductor": "Bioconductor registry support", + "bsd": "FreeBSD packages, proposed in purl-spec#496", "clojars": "Clojars registry support", "deno": "Deno registry support", "elm": "Elm registry support", diff --git a/types.json b/types.json index 5bc744d..1c77a64 100644 --- a/types.json +++ b/types.json @@ -62,6 +62,15 @@ "pkg:brew/go" ] }, + "bsd": { + "description": "FreeBSD and other BSD operating system packages", + "default_registry": null, + "namespace_requirement": "required", + "examples": [ + "pkg:bsd/freebsd/emacs@30.1_2?distro=14.3&epoch=3", + "pkg:bsd/freebsd/python311@3.11.13?distro=14.3" + ] + }, "cargo": { "description": "Cargo packages for Rust", "default_registry": "https://crates.io", From 1bcf8f59a9fd2b6bb445f6894aa5bf30871b120f Mon Sep 17 00:00:00 2001 From: Simon Chrzanowski Date: Wed, 21 Oct 2026 12:21:54 +0200 Subject: [PATCH 2/2] Lowercase the bsd namespace The bsd type in purl-spec#496 declares the namespace case-insensitive. packageurl-go doesn't know the type, so lowercase it in lowercaseCaseInsensitive like vscode-extension. --- ecosystem.go | 1 + ecosystem_test.go | 15 +++++++++++++++ purl.go | 5 ++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/ecosystem.go b/ecosystem.go index 1251924..8bd54cb 100644 --- a/ecosystem.go +++ b/ecosystem.go @@ -20,6 +20,7 @@ const ( purlTypeGem = "gem" purlTypeGitHubActions = "githubactions" purlTypeVSCodeExtension = "vscode-extension" + purlTypeBSD = "bsd" ) // purlTypeForEcosystem maps ecosystem names to PURL types. diff --git a/ecosystem_test.go b/ecosystem_test.go index 7775754..c274548 100644 --- a/ecosystem_test.go +++ b/ecosystem_test.go @@ -336,3 +336,18 @@ func TestFreeBSDPURL(t *testing.T) { t.Error("bsd type must require a namespace") } } + +func TestFreeBSDNamespaceCase(t *testing.T) { + // The bsd namespace is case-insensitive and lowercased; the name keeps its case. + const want = "pkg:bsd/freebsd/p5-Mojolicious@9.39" + parsed, err := Parse("pkg:bsd/FreeBSD/p5-Mojolicious@9.39") + if err != nil { + t.Fatal(err) + } + if got := parsed.String(); got != want { + t.Errorf("Parse().String() = %q, want %q", got, want) + } + if got := New("bsd", "FreeBSD", "p5-Mojolicious", "9.39", nil).String(); got != want { + t.Errorf("New().String() = %q, want %q", got, want) + } +} diff --git a/purl.go b/purl.go index 3402bb0..769bbaa 100644 --- a/purl.go +++ b/purl.go @@ -40,9 +40,12 @@ func Parse(s string) (*PURL, error) { // lowercaseCaseInsensitive lowercases components the spec declares // case-insensitive where packageurl-go's Normalize leaves them as given. func lowercaseCaseInsensitive(p *packageurl.PackageURL) { - if p.Type == purlTypeVSCodeExtension { + switch p.Type { + case purlTypeVSCodeExtension: p.Namespace = strings.ToLower(p.Namespace) p.Name = strings.ToLower(p.Name) + case purlTypeBSD: + p.Namespace = strings.ToLower(p.Namespace) } }