Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ output stays stable for callers that store it:

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. `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.
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/...`. `terraform` follows the type proposed in [purl-spec#889](https://github.com/package-url/purl-spec/pull/889): a provider address `namespace/type` maps to `pkg:terraform/{namespace}/{type}`, a module address `namespace/name/system` to `pkg:terraform/{namespace}/{name}/{system}`, and the default registry is `https://registry.terraform.io`. 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
Expand Down
1 change: 1 addition & 0 deletions ecosystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
purlTypeGitHubActions = "githubactions"
purlTypeVSCodeExtension = "vscode-extension"
purlTypeBSD = "bsd"
purlTypeTerraform = "terraform"
)

// purlTypeForEcosystem maps ecosystem names to PURL types.
Expand Down
14 changes: 14 additions & 0 deletions makepurl.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ func splitNamespace(ecosystem, name string) (namespace, pkgName string, ok bool)
}
case purlTypeVSCodeExtension:
return splitVSCodeExtension(name)
case purlTypeTerraform:
return splitTerraform(name)
case ecosystemSwift:
return splitSwift(name)
}
Expand Down Expand Up @@ -269,6 +271,18 @@ func splitSwift(name string) (namespace, pkgName string, ok bool) {
}

// splitVSCodeExtension splits VS Code's publisher.name extension identifier.
// splitTerraform splits a Terraform address without its registry hostname:
// namespace/type for a provider, namespace/name/system for a module, whose
// namespace is then namespace/name. The type requires a namespace, so an
// address without one, or with an empty segment, is rejected.
func splitTerraform(name string) (namespace, pkgName string, ok bool) {
i := strings.LastIndexByte(name, '/')
if i <= 0 || i == len(name)-1 || name[0] == '/' || strings.Contains(name, "//") {
return "", "", false
}
return name[:i], name[i+1:], true
}

func splitVSCodeExtension(name string) (publisher, extension string, ok bool) {
publisher, extension, ok = strings.Cut(name, ".")
if !ok || publisher == "" || extension == "" {
Expand Down
15 changes: 15 additions & 0 deletions makepurl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ func TestBuildPURLString(t *testing.T) {
{"swift registry identity", "swift", "apple.swift-argument-parser", "1.8.2", "", "pkg:swift/apple/swift-argument-parser@1.8.2"},
{"swift registry path", "swift", "apple/swift-argument-parser", "1.8.2", "", ""},
{"swift registry URL path", "swift", "/apple/swift-argument-parser", "1.8.2", "", ""},
{"terraform provider", "terraform", "hashicorp/random", "3.6.0", "", "pkg:terraform/hashicorp/random@3.6.0"},
{"terraform module", "terraform", "hashicorp/consul/aws", "0.11.0", "", "pkg:terraform/hashicorp/consul/aws@0.11.0"},
{"terraform case-sensitive", "terraform", "HashiCorp/Random", "3.6.0", "", "pkg:terraform/HashiCorp/Random@3.6.0"},
{"terraform default registry ignored", "terraform", "hashicorp/random", "3.6.0", "https://registry.terraform.io", "pkg:terraform/hashicorp/random@3.6.0"},
{"terraform other registry", "terraform", "hashicorp/random", "3.6.0", "https://registry.opentofu.org", "pkg:terraform/hashicorp/random@3.6.0?repository_url=https:%2F%2Fregistry.opentofu.org"},
{"terraform without namespace", "terraform", "random", "3.6.0", "", ""},
{"terraform empty name", "terraform", "hashicorp/", "3.6.0", "", ""},
{"terraform empty namespace", "terraform", "/random", "3.6.0", "", ""},
{"terraform slash-only namespace", "terraform", "//random", "3.6.0", "", ""},
{"terraform leading slash", "terraform", "/hashicorp/random", "3.6.0", "", ""},
{"terraform empty segment", "terraform", "hashicorp//random", "3.6.0", "", ""},
{"terraform module empty segment", "terraform", "hashicorp/consul//aws", "0.11.0", "", ""},
}

for _, tt := range tests {
Expand Down Expand Up @@ -126,6 +138,9 @@ func TestBuildPURLStringMatchesMakePURL(t *testing.T) {
{"npm", "lodash", "^1.0.0", ""},
{"npm", "pkg", "1.0.0", "https://custom.registry.com"},
{"swift", "github.com/apple/swift-argument-parser", "1.8.2", ""},
{"terraform", "hashicorp/random", "3.6.0", ""},
{"terraform", "hashicorp/consul/aws", "0.11.0", ""},
{"terraform", "hashicorp/random", "3.6.0", "https://registry.opentofu.org"},
}

for _, tt := range cases {
Expand Down
1 change: 1 addition & 0 deletions spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func TestSpecTypeMetadata(t *testing.T) {
"deno": "Deno registry support",
"elm": "Elm registry support",
"homebrew": "Legacy type retained alongside brew for compatibility",
"terraform": "Terraform providers and modules, proposed in purl-spec#889",
}
for _, name := range purl.KnownTypes() {
if !registered[name] && localTypes[name] == "" {
Expand Down
9 changes: 9 additions & 0 deletions types.json
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,15 @@
}
}
},
"terraform": {
"description": "Terraform providers and modules, proposed in purl-spec#889",
"default_registry": "https://registry.terraform.io",
"namespace_requirement": "required",
"examples": [
"pkg:terraform/hashicorp/aws@6.0.0",
"pkg:terraform/terraform-aws-modules/vpc@6.0.1"
]
},
"vcpkg": {
"description": "Packages from the vcpkg C/C++ package manager.",
"default_registry": null,
Expand Down
3 changes: 3 additions & 0 deletions types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func TestIsKnownType(t *testing.T) {
{"npm", true},
{"pypi", true},
{"cargo", true},
{"terraform", true},
{"unknown-type", false},
{"", false},
}
Expand All @@ -102,6 +103,7 @@ func TestDefaultRegistry(t *testing.T) {
{"npm", "https://registry.npmjs.org"},
{"pypi", "https://pypi.org"},
{"cargo", "https://crates.io"},
{"terraform", "https://registry.terraform.io"},
{"apk", ""}, // no default registry
{"deb", ""}, // no default registry
{"unknown", ""},
Expand All @@ -128,6 +130,7 @@ func TestNamespaceRequirement(t *testing.T) {
{"cran", false, true},
{"npm", false, false}, // optional
{"cargo", false, true},
{"terraform", true, false},
}

for _, tt := range tests {
Expand Down
Loading