purl 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +81 -1
- data/exe/purl +70 -0
- data/lib/purl/download_url.rb +266 -0
- data/lib/purl/version.rb +1 -1
- data/lib/purl.rb +13 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02a4895e51ce7c9ace65a53d97fd8d4c5d288d7137daa71c9e8b8a778595e6cb
|
|
4
|
+
data.tar.gz: 55d348a442e23a0ffddc2b236bc1be22c9cf29d8eac0ce3ff289ce637e1ba6f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f8ff4ff13c1184c4b1adf6950fc1d04acedb8ce2cbbf4d62f2430455567e98f8b153b5493732919e1921a55132a2f3f51b61157b6eba85e207e6675d8675771
|
|
7
|
+
data.tar.gz: 9f203b73ef705fe70a27de5aae3b85558bd34204a2362a7429c441dc7f66d4a5ae403741fcb36471ab33e972868e4d31e6121a5eeaf23fb88832e3d38fec1e9c
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.7.0] - 2026-01-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Download URL generation for 18 package ecosystems
|
|
14
|
+
- `download_url` instance method on `PackageURL` for getting artifact download URLs
|
|
15
|
+
- `supports_download_url?` method to check if download URL generation is supported
|
|
16
|
+
- `Purl.download_supported_types` to list all types with download URL support
|
|
17
|
+
- `download` command in CLI for getting download URLs from PURLs
|
|
18
|
+
- Supported types: bioconductor, bitbucket, cargo, clojars, cran, elm, gem, github, gitlab, golang, hackage, hex, luarocks, maven, npm, nuget, pub, swift
|
|
19
|
+
- Support for `repository_url` qualifier to use custom registries for downloads
|
|
20
|
+
- Support for custom base URLs via parameter override
|
|
21
|
+
|
|
10
22
|
## [1.6.0] - 2025-10-24
|
|
11
23
|
|
|
12
24
|
### Added
|
data/README.md
CHANGED
|
@@ -16,10 +16,11 @@ This library features comprehensive error handling with namespaced error types,
|
|
|
16
16
|
|
|
17
17
|
## Features
|
|
18
18
|
|
|
19
|
-
- **Command-line interface** with parse, validate, convert, generate, info, lookup, and advisories commands plus JSON output
|
|
19
|
+
- **Command-line interface** with parse, validate, convert, download, generate, info, lookup, and advisories commands plus JSON output
|
|
20
20
|
- **Comprehensive PURL parsing and validation** with 37 package types (32 official + 5 additional ecosystems)
|
|
21
21
|
- **Better error handling** with namespaced error classes and contextual information
|
|
22
22
|
- **Bidirectional registry URL conversion** - generate registry URLs from PURLs and parse PURLs from registry URLs
|
|
23
|
+
- **Download URL generation** for 18 package ecosystems (gem, npm, cargo, maven, etc.)
|
|
23
24
|
- **Security advisory lookup** - query security advisories from advisories.ecosyste.ms
|
|
24
25
|
- **Package information lookup** - query package metadata from ecosyste.ms
|
|
25
26
|
- **Type-specific validation** for conan, cran, and swift packages
|
|
@@ -69,6 +70,7 @@ purl parse <purl-string> # Parse and display PURL components
|
|
|
69
70
|
purl validate <purl-string> # Validate a PURL (exit code indicates success)
|
|
70
71
|
purl convert <registry-url> # Convert registry URL to PURL
|
|
71
72
|
purl url <purl-string> # Convert PURL to registry URL
|
|
73
|
+
purl download <purl-string> # Get download URL for package version
|
|
72
74
|
purl generate [options] # Generate PURL from components
|
|
73
75
|
purl info [type] # Show information about PURL types
|
|
74
76
|
purl lookup <purl-string> # Look up package information from ecosyste.ms
|
|
@@ -150,6 +152,26 @@ $ purl --json url "pkg:gem/rails@7.0.0"
|
|
|
150
152
|
}
|
|
151
153
|
```
|
|
152
154
|
|
|
155
|
+
#### Get Download URL
|
|
156
|
+
```bash
|
|
157
|
+
$ purl download "pkg:gem/rails@7.0.0"
|
|
158
|
+
https://rubygems.org/downloads/rails-7.0.0.gem
|
|
159
|
+
|
|
160
|
+
$ purl download "pkg:npm/@babel/core@7.20.0"
|
|
161
|
+
https://registry.npmjs.org/@babel/core/-/core-7.20.0.tgz
|
|
162
|
+
|
|
163
|
+
$ purl download "pkg:cargo/serde@1.0.152"
|
|
164
|
+
https://static.crates.io/crates/serde/serde-1.0.152.crate
|
|
165
|
+
|
|
166
|
+
$ purl --json download "pkg:maven/org.apache.commons/commons-lang3@3.12.0"
|
|
167
|
+
{
|
|
168
|
+
"success": true,
|
|
169
|
+
"purl": "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
|
|
170
|
+
"download_url": "https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar",
|
|
171
|
+
"type": "maven"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
153
175
|
#### Generate a PURL
|
|
154
176
|
```bash
|
|
155
177
|
$ purl generate --type gem --name rails --version 7.0.0
|
|
@@ -408,6 +430,42 @@ purl = Purl.parse("pkg:npm/@babel/core@7.0.0")
|
|
|
408
430
|
puts purl.registry_url # => "https://www.npmjs.com/package/@babel/core"
|
|
409
431
|
```
|
|
410
432
|
|
|
433
|
+
### Download URL Generation
|
|
434
|
+
|
|
435
|
+
Generate direct download URLs for package artifacts:
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
# Generate download URLs from PURLs
|
|
439
|
+
purl = Purl.parse("pkg:gem/rails@7.0.0")
|
|
440
|
+
puts purl.download_url # => "https://rubygems.org/downloads/rails-7.0.0.gem"
|
|
441
|
+
|
|
442
|
+
# Check if download URL generation is supported
|
|
443
|
+
puts purl.supports_download_url? # => true
|
|
444
|
+
|
|
445
|
+
# NPM with scoped packages
|
|
446
|
+
purl = Purl.parse("pkg:npm/@babel/core@7.20.0")
|
|
447
|
+
puts purl.download_url # => "https://registry.npmjs.org/@babel/core/-/core-7.20.0.tgz"
|
|
448
|
+
|
|
449
|
+
# Maven packages
|
|
450
|
+
purl = Purl.parse("pkg:maven/org.apache.commons/commons-lang3@3.12.0")
|
|
451
|
+
puts purl.download_url # => "https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar"
|
|
452
|
+
|
|
453
|
+
# Custom registry via repository_url qualifier
|
|
454
|
+
purl = Purl.parse("pkg:npm/lodash@4.17.21?repository_url=https://npm.mycompany.com")
|
|
455
|
+
puts purl.download_url # => "https://npm.mycompany.com/lodash/-/lodash-4.17.21.tgz"
|
|
456
|
+
|
|
457
|
+
# Custom registry via parameter
|
|
458
|
+
purl = Purl.parse("pkg:gem/rails@7.0.0")
|
|
459
|
+
puts purl.download_url(base_url: "https://gems.internal.com/downloads")
|
|
460
|
+
# => "https://gems.internal.com/downloads/rails-7.0.0.gem"
|
|
461
|
+
|
|
462
|
+
# Get list of supported types
|
|
463
|
+
puts Purl.download_supported_types
|
|
464
|
+
# => ["bioconductor", "bitbucket", "cargo", "clojars", "cran", "elm", "gem",
|
|
465
|
+
# "github", "gitlab", "golang", "hackage", "hex", "luarocks", "maven",
|
|
466
|
+
# "npm", "nuget", "pub", "swift"]
|
|
467
|
+
```
|
|
468
|
+
|
|
411
469
|
### Reverse Parsing: Registry URLs to PURLs
|
|
412
470
|
|
|
413
471
|
```ruby
|
|
@@ -501,6 +559,7 @@ puts Purl.known_types.include?("gem") # => true
|
|
|
501
559
|
puts Purl.known_type?("gem") # => true
|
|
502
560
|
puts Purl.registry_supported_types # => ["cargo", "gem", "maven", "npm", ...]
|
|
503
561
|
puts Purl.reverse_parsing_supported_types # => ["bioconductor", "cargo", "clojars", ...]
|
|
562
|
+
puts Purl.download_supported_types # => ["cargo", "gem", "maven", "npm", ...]
|
|
504
563
|
|
|
505
564
|
# Get default registry for a type
|
|
506
565
|
puts Purl.default_registry("gem") # => "https://rubygems.org"
|
|
@@ -520,6 +579,7 @@ puts info[:default_registry] # => "https://rubygems.org"
|
|
|
520
579
|
puts info[:examples] # => ["pkg:gem/rails@7.0.4", ...]
|
|
521
580
|
puts info[:registry_url_generation] # => true
|
|
522
581
|
puts info[:reverse_parsing] # => true
|
|
582
|
+
puts info[:download_url_generation] # => true
|
|
523
583
|
puts info[:route_patterns] # => ["https://rubygems.org/gems/:name", ...]
|
|
524
584
|
```
|
|
525
585
|
|
|
@@ -610,6 +670,26 @@ The library supports 37 package types (32 official + 5 additional ecosystems):
|
|
|
610
670
|
**Reverse Parsing (20 types):**
|
|
611
671
|
- `bioconductor`, `cargo`, `clojars`, `cocoapods`, `composer`, `conda`, `cpan`, `deno`, `elm`, `gem`, `golang`, `hackage`, `hex`, `homebrew`, `maven`, `npm`, `nuget`, `pub`, `pypi`, `swift`
|
|
612
672
|
|
|
673
|
+
**Download URL Generation (18 types):**
|
|
674
|
+
- `bioconductor` - bioconductor.org/packages/release/bioc/src/contrib
|
|
675
|
+
- `bitbucket` - bitbucket.org archive downloads
|
|
676
|
+
- `cargo` - static.crates.io/crates
|
|
677
|
+
- `clojars` - repo.clojars.org (Maven-style)
|
|
678
|
+
- `cran` - cran.r-project.org/src/contrib
|
|
679
|
+
- `elm` - GitHub archive downloads
|
|
680
|
+
- `gem` - rubygems.org/downloads
|
|
681
|
+
- `github` - GitHub archive downloads
|
|
682
|
+
- `gitlab` - GitLab archive downloads
|
|
683
|
+
- `golang` - proxy.golang.org
|
|
684
|
+
- `hackage` - hackage.haskell.org/package
|
|
685
|
+
- `hex` - repo.hex.pm/tarballs
|
|
686
|
+
- `luarocks` - luarocks.org/manifests
|
|
687
|
+
- `maven` - repo.maven.apache.org/maven2
|
|
688
|
+
- `npm` - registry.npmjs.org
|
|
689
|
+
- `nuget` - api.nuget.org/v3-flatcontainer
|
|
690
|
+
- `pub` - pub.dev/packages
|
|
691
|
+
- `swift` - GitHub/GitLab/Bitbucket (derived from namespace)
|
|
692
|
+
|
|
613
693
|
**All 37 Supported Types:**
|
|
614
694
|
`alpm`, `apk`, `bioconductor`, `bitbucket`, `bitnami`, `cargo`, `clojars`, `cocoapods`, `composer`, `conan`, `conda`, `cpan`, `cran`, `deb`, `deno`, `docker`, `elm`, `gem`, `generic`, `github`, `golang`, `hackage`, `hex`, `homebrew`, `huggingface`, `luarocks`, `maven`, `mlflow`, `npm`, `nuget`, `oci`, `pub`, `pypi`, `qpkg`, `rpm`, `swid`, `swift`
|
|
615
695
|
|
data/exe/purl
CHANGED
|
@@ -40,6 +40,8 @@ class PurlCLI
|
|
|
40
40
|
generate_command(args)
|
|
41
41
|
when "url"
|
|
42
42
|
url_command(args)
|
|
43
|
+
when "download"
|
|
44
|
+
download_command(args)
|
|
43
45
|
when "info"
|
|
44
46
|
info_command(args)
|
|
45
47
|
when "lookup"
|
|
@@ -70,6 +72,7 @@ class PurlCLI
|
|
|
70
72
|
purl [--json] validate <purl-string> Validate a PURL (exit code indicates success)
|
|
71
73
|
purl [--json] convert <registry-url> Convert registry URL to PURL
|
|
72
74
|
purl [--json] url <purl-string> Convert PURL to registry URL
|
|
75
|
+
purl [--json] download <purl-string> Get download URL for package version
|
|
73
76
|
purl [--json] generate [options] Generate PURL from components
|
|
74
77
|
purl [--json] info [type] Show information about PURL types
|
|
75
78
|
purl [--json] lookup <purl-string> Look up package information from ecosyste.ms
|
|
@@ -277,6 +280,73 @@ class PurlCLI
|
|
|
277
280
|
end
|
|
278
281
|
end
|
|
279
282
|
|
|
283
|
+
def download_command(args)
|
|
284
|
+
if args.empty?
|
|
285
|
+
output_error("PURL string required")
|
|
286
|
+
exit 1
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
purl_string = args[0]
|
|
290
|
+
|
|
291
|
+
begin
|
|
292
|
+
purl = Purl.parse(purl_string)
|
|
293
|
+
|
|
294
|
+
unless purl.supports_download_url?
|
|
295
|
+
if @json_output
|
|
296
|
+
result = {
|
|
297
|
+
success: false,
|
|
298
|
+
purl: purl_string,
|
|
299
|
+
error: "Download URL generation not supported for type '#{purl.type}'",
|
|
300
|
+
supported_types: Purl.download_supported_types
|
|
301
|
+
}
|
|
302
|
+
puts JSON.pretty_generate(result)
|
|
303
|
+
else
|
|
304
|
+
puts "Error: Download URL generation not supported for type '#{purl.type}'"
|
|
305
|
+
puts "Supported types: #{Purl.download_supported_types.join(', ')}"
|
|
306
|
+
end
|
|
307
|
+
exit 1
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
download_url = purl.download_url
|
|
311
|
+
|
|
312
|
+
if @json_output
|
|
313
|
+
result = {
|
|
314
|
+
success: true,
|
|
315
|
+
purl: purl.to_s,
|
|
316
|
+
download_url: download_url,
|
|
317
|
+
type: purl.type
|
|
318
|
+
}
|
|
319
|
+
puts JSON.pretty_generate(result)
|
|
320
|
+
else
|
|
321
|
+
puts download_url
|
|
322
|
+
end
|
|
323
|
+
rescue Purl::MissingVersionError => e
|
|
324
|
+
if @json_output
|
|
325
|
+
result = {
|
|
326
|
+
success: false,
|
|
327
|
+
purl: purl_string,
|
|
328
|
+
error: "Version required for download URL"
|
|
329
|
+
}
|
|
330
|
+
puts JSON.pretty_generate(result)
|
|
331
|
+
else
|
|
332
|
+
puts "Error: Version required for download URL"
|
|
333
|
+
end
|
|
334
|
+
exit 1
|
|
335
|
+
rescue Purl::Error => e
|
|
336
|
+
if @json_output
|
|
337
|
+
result = {
|
|
338
|
+
success: false,
|
|
339
|
+
purl: purl_string,
|
|
340
|
+
error: e.message
|
|
341
|
+
}
|
|
342
|
+
puts JSON.pretty_generate(result)
|
|
343
|
+
else
|
|
344
|
+
puts "Error: #{e.message}"
|
|
345
|
+
end
|
|
346
|
+
exit 1
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
280
350
|
def generate_command(args)
|
|
281
351
|
options = {}
|
|
282
352
|
OptionParser.new do |opts|
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Purl
|
|
4
|
+
class DownloadURL
|
|
5
|
+
DOWNLOAD_PATTERNS = {
|
|
6
|
+
"gem" => {
|
|
7
|
+
base_url: "https://rubygems.org/downloads",
|
|
8
|
+
pattern: ->(purl) { "#{purl.name}-#{purl.version}.gem" }
|
|
9
|
+
},
|
|
10
|
+
"npm" => {
|
|
11
|
+
base_url: "https://registry.npmjs.org",
|
|
12
|
+
pattern: ->(purl) do
|
|
13
|
+
# npm: /{name}/-/{basename}-{version}.tgz
|
|
14
|
+
# For scoped packages: /@scope/name/-/name-version.tgz
|
|
15
|
+
basename = purl.name.split("/").last
|
|
16
|
+
if purl.namespace
|
|
17
|
+
"#{purl.namespace}/#{purl.name}/-/#{basename}-#{purl.version}.tgz"
|
|
18
|
+
else
|
|
19
|
+
"#{purl.name}/-/#{purl.name}-#{purl.version}.tgz"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
},
|
|
23
|
+
"cargo" => {
|
|
24
|
+
base_url: "https://static.crates.io/crates",
|
|
25
|
+
pattern: ->(purl) { "#{purl.name}/#{purl.name}-#{purl.version}.crate" }
|
|
26
|
+
},
|
|
27
|
+
"nuget" => {
|
|
28
|
+
base_url: "https://api.nuget.org/v3-flatcontainer",
|
|
29
|
+
pattern: ->(purl) do
|
|
30
|
+
name_lower = purl.name.downcase
|
|
31
|
+
"#{name_lower}/#{purl.version}/#{name_lower}.#{purl.version}.nupkg"
|
|
32
|
+
end
|
|
33
|
+
},
|
|
34
|
+
"hex" => {
|
|
35
|
+
base_url: "https://repo.hex.pm/tarballs",
|
|
36
|
+
pattern: ->(purl) { "#{purl.name}-#{purl.version}.tar" }
|
|
37
|
+
},
|
|
38
|
+
"hackage" => {
|
|
39
|
+
base_url: "https://hackage.haskell.org/package",
|
|
40
|
+
pattern: ->(purl) { "#{purl.name}-#{purl.version}/#{purl.name}-#{purl.version}.tar.gz" }
|
|
41
|
+
},
|
|
42
|
+
"pub" => {
|
|
43
|
+
base_url: "https://pub.dev/packages",
|
|
44
|
+
pattern: ->(purl) { "#{purl.name}/versions/#{purl.version}.tar.gz" }
|
|
45
|
+
},
|
|
46
|
+
"golang" => {
|
|
47
|
+
base_url: "https://proxy.golang.org",
|
|
48
|
+
pattern: ->(purl) do
|
|
49
|
+
# Go module proxy requires encoding capital letters as !lowercase
|
|
50
|
+
full_path = purl.namespace ? "#{purl.namespace}/#{purl.name}" : purl.name
|
|
51
|
+
encoded = full_path.gsub(/[A-Z]/) { |s| "!#{s.downcase}" }
|
|
52
|
+
"#{encoded}/@v/#{purl.version}.zip"
|
|
53
|
+
end
|
|
54
|
+
},
|
|
55
|
+
"maven" => {
|
|
56
|
+
base_url: "https://repo.maven.apache.org/maven2",
|
|
57
|
+
pattern: ->(purl) do
|
|
58
|
+
# Maven: /{group_path}/{artifact}/{version}/{artifact}-{version}.jar
|
|
59
|
+
# group_id uses dots, path uses slashes
|
|
60
|
+
group_path = purl.namespace.gsub(".", "/")
|
|
61
|
+
"#{group_path}/#{purl.name}/#{purl.version}/#{purl.name}-#{purl.version}.jar"
|
|
62
|
+
end
|
|
63
|
+
},
|
|
64
|
+
"cran" => {
|
|
65
|
+
base_url: "https://cran.r-project.org/src/contrib",
|
|
66
|
+
pattern: ->(purl) { "#{purl.name}_#{purl.version}.tar.gz" }
|
|
67
|
+
},
|
|
68
|
+
"bioconductor" => {
|
|
69
|
+
base_url: "https://bioconductor.org/packages/release/bioc/src/contrib",
|
|
70
|
+
pattern: ->(purl) { "#{purl.name}_#{purl.version}.tar.gz" }
|
|
71
|
+
},
|
|
72
|
+
"clojars" => {
|
|
73
|
+
base_url: "https://repo.clojars.org",
|
|
74
|
+
pattern: ->(purl) do
|
|
75
|
+
# Clojars uses maven-style paths
|
|
76
|
+
# namespace is group_id, name is artifact_id
|
|
77
|
+
# If no namespace, group_id = artifact_id
|
|
78
|
+
group_id = purl.namespace || purl.name
|
|
79
|
+
artifact_id = purl.name
|
|
80
|
+
group_path = group_id.gsub(".", "/")
|
|
81
|
+
"#{group_path}/#{artifact_id}/#{purl.version}/#{artifact_id}-#{purl.version}.jar"
|
|
82
|
+
end
|
|
83
|
+
},
|
|
84
|
+
"elm" => {
|
|
85
|
+
base_url: "https://github.com",
|
|
86
|
+
pattern: ->(purl) do
|
|
87
|
+
# Elm packages are hosted on GitHub
|
|
88
|
+
# namespace/name format maps to GitHub user/repo
|
|
89
|
+
return nil unless purl.namespace
|
|
90
|
+
"#{purl.namespace}/#{purl.name}/archive/#{purl.version}.zip"
|
|
91
|
+
end
|
|
92
|
+
},
|
|
93
|
+
"github" => {
|
|
94
|
+
base_url: "https://github.com",
|
|
95
|
+
pattern: ->(purl) do
|
|
96
|
+
return nil unless purl.namespace
|
|
97
|
+
"#{purl.namespace}/#{purl.name}/archive/refs/tags/#{purl.version}.tar.gz"
|
|
98
|
+
end
|
|
99
|
+
},
|
|
100
|
+
"gitlab" => {
|
|
101
|
+
base_url: "https://gitlab.com",
|
|
102
|
+
pattern: ->(purl) do
|
|
103
|
+
return nil unless purl.namespace
|
|
104
|
+
"#{purl.namespace}/#{purl.name}/-/archive/#{purl.version}/#{purl.name}-#{purl.version}.tar.gz"
|
|
105
|
+
end
|
|
106
|
+
},
|
|
107
|
+
"bitbucket" => {
|
|
108
|
+
base_url: "https://bitbucket.org",
|
|
109
|
+
pattern: ->(purl) do
|
|
110
|
+
return nil unless purl.namespace
|
|
111
|
+
"#{purl.namespace}/#{purl.name}/get/#{purl.version}.tar.gz"
|
|
112
|
+
end
|
|
113
|
+
},
|
|
114
|
+
"luarocks" => {
|
|
115
|
+
base_url: "https://luarocks.org/manifests",
|
|
116
|
+
pattern: ->(purl) do
|
|
117
|
+
return nil unless purl.namespace
|
|
118
|
+
"#{purl.namespace}/#{purl.name}-#{purl.version}.src.rock"
|
|
119
|
+
end
|
|
120
|
+
},
|
|
121
|
+
"swift" => {
|
|
122
|
+
base_url: nil,
|
|
123
|
+
pattern: ->(purl) do
|
|
124
|
+
# Swift namespace is like "github.com/owner", name is repo
|
|
125
|
+
# e.g. pkg:swift/github.com/Alamofire/Alamofire@5.6.4
|
|
126
|
+
return nil unless purl.namespace
|
|
127
|
+
parts = purl.namespace.split("/", 2)
|
|
128
|
+
host = parts[0]
|
|
129
|
+
owner = parts[1]
|
|
130
|
+
return nil unless host && owner
|
|
131
|
+
|
|
132
|
+
case host
|
|
133
|
+
when "github.com"
|
|
134
|
+
"https://github.com/#{owner}/#{purl.name}/archive/refs/tags/#{purl.version}.tar.gz"
|
|
135
|
+
when "gitlab.com"
|
|
136
|
+
"https://gitlab.com/#{owner}/#{purl.name}/-/archive/#{purl.version}/#{purl.name}-#{purl.version}.tar.gz"
|
|
137
|
+
when "bitbucket.org"
|
|
138
|
+
"https://bitbucket.org/#{owner}/#{purl.name}/get/#{purl.version}.tar.gz"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
},
|
|
142
|
+
"composer" => {
|
|
143
|
+
base_url: nil,
|
|
144
|
+
pattern: ->(purl) { nil },
|
|
145
|
+
note: "Composer packages are downloaded from source repositories, not Packagist"
|
|
146
|
+
},
|
|
147
|
+
"cocoapods" => {
|
|
148
|
+
base_url: nil,
|
|
149
|
+
pattern: ->(purl) { nil },
|
|
150
|
+
note: "CocoaPods packages are downloaded from source repositories"
|
|
151
|
+
}
|
|
152
|
+
}.freeze
|
|
153
|
+
|
|
154
|
+
def self.generate(purl, base_url: nil)
|
|
155
|
+
new(purl).generate(base_url: base_url)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Types that require a namespace for download URLs
|
|
159
|
+
NAMESPACE_REQUIRED_TYPES = %w[maven elm github gitlab bitbucket luarocks swift].freeze
|
|
160
|
+
|
|
161
|
+
def self.supported_types
|
|
162
|
+
DOWNLOAD_PATTERNS.keys.select do |k|
|
|
163
|
+
pattern = DOWNLOAD_PATTERNS[k]
|
|
164
|
+
# Skip types with notes (they're not really supported)
|
|
165
|
+
next false if pattern[:note]
|
|
166
|
+
|
|
167
|
+
# Test with appropriate namespace for types that need it
|
|
168
|
+
namespace = if NAMESPACE_REQUIRED_TYPES.include?(k)
|
|
169
|
+
k == "swift" ? "github.com/test" : "test"
|
|
170
|
+
end
|
|
171
|
+
begin
|
|
172
|
+
result = pattern[:pattern].call(Purl::PackageURL.new(type: k, name: "test", version: "1.0", namespace: namespace))
|
|
173
|
+
!result.nil?
|
|
174
|
+
rescue
|
|
175
|
+
false
|
|
176
|
+
end
|
|
177
|
+
end.sort
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def self.supports?(type)
|
|
181
|
+
pattern = DOWNLOAD_PATTERNS[type.to_s.downcase]
|
|
182
|
+
return false unless pattern
|
|
183
|
+
# Types with base_url are supported, or types that return full URLs (like swift)
|
|
184
|
+
return true if pattern[:base_url]
|
|
185
|
+
# Check if this type returns full URLs by testing with a sample
|
|
186
|
+
return false if pattern[:note] # Has a note means it's not really supported
|
|
187
|
+
true
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def initialize(purl)
|
|
191
|
+
@purl = purl
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def generate(base_url: nil)
|
|
195
|
+
unless @purl.version
|
|
196
|
+
raise MissingVersionError.new(
|
|
197
|
+
"Download URL requires a version",
|
|
198
|
+
type: @purl.type
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
pattern_config = DOWNLOAD_PATTERNS[@purl.type.downcase]
|
|
203
|
+
|
|
204
|
+
unless pattern_config
|
|
205
|
+
raise UnsupportedTypeError.new(
|
|
206
|
+
"No download URL pattern defined for type '#{@purl.type}'. Supported types: #{self.class.supported_types.join(", ")}",
|
|
207
|
+
type: @purl.type,
|
|
208
|
+
supported_types: self.class.supported_types
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Check for repository_url qualifier in the PURL
|
|
213
|
+
qualifier_base_url = @purl.qualifiers&.dig("repository_url")
|
|
214
|
+
|
|
215
|
+
# Generate the path/URL from the pattern
|
|
216
|
+
path = pattern_config[:pattern].call(@purl)
|
|
217
|
+
|
|
218
|
+
if path.nil?
|
|
219
|
+
raise UnsupportedTypeError.new(
|
|
220
|
+
"Could not generate download URL for '#{@purl.type}'. #{pattern_config[:note]}",
|
|
221
|
+
type: @purl.type,
|
|
222
|
+
supported_types: self.class.supported_types
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# If the pattern returns a full URL, use it directly
|
|
227
|
+
return path if path.start_with?("http://", "https://")
|
|
228
|
+
|
|
229
|
+
# For relative paths, we need a base_url
|
|
230
|
+
effective_base_url = base_url || qualifier_base_url || pattern_config[:base_url]
|
|
231
|
+
|
|
232
|
+
unless effective_base_url
|
|
233
|
+
raise UnsupportedTypeError.new(
|
|
234
|
+
"Download URLs are not available for type '#{@purl.type}'. #{pattern_config[:note]}",
|
|
235
|
+
type: @purl.type,
|
|
236
|
+
supported_types: self.class.supported_types
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
"#{effective_base_url}/#{path}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
attr_reader :purl
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Error for missing version
|
|
247
|
+
class MissingVersionError < Error
|
|
248
|
+
attr_reader :type
|
|
249
|
+
|
|
250
|
+
def initialize(message, type: nil)
|
|
251
|
+
@type = type
|
|
252
|
+
super(message)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Add download URL generation methods to PackageURL
|
|
257
|
+
class PackageURL
|
|
258
|
+
def download_url(base_url: nil)
|
|
259
|
+
DownloadURL.generate(self, base_url: base_url)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def supports_download_url?
|
|
263
|
+
DownloadURL.supports?(type)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
data/lib/purl/version.rb
CHANGED
data/lib/purl.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "purl/version"
|
|
|
4
4
|
require_relative "purl/errors"
|
|
5
5
|
require_relative "purl/package_url"
|
|
6
6
|
require_relative "purl/registry_url"
|
|
7
|
+
require_relative "purl/download_url"
|
|
7
8
|
require_relative "purl/lookup"
|
|
8
9
|
require_relative "purl/lookup_formatter"
|
|
9
10
|
require_relative "purl/advisory"
|
|
@@ -102,6 +103,17 @@ module Purl
|
|
|
102
103
|
RegistryURL.supported_reverse_types
|
|
103
104
|
end
|
|
104
105
|
|
|
106
|
+
# Returns types that have download URL support
|
|
107
|
+
#
|
|
108
|
+
# @return [Array<String>] sorted array of types that can generate download URLs
|
|
109
|
+
#
|
|
110
|
+
# @example
|
|
111
|
+
# types = Purl.download_supported_types
|
|
112
|
+
# puts types.include?("gem") # true if gem has download URL support
|
|
113
|
+
def self.download_supported_types
|
|
114
|
+
DownloadURL.supported_types
|
|
115
|
+
end
|
|
116
|
+
|
|
105
117
|
# Check if a type is known/valid
|
|
106
118
|
#
|
|
107
119
|
# @param type [String, Symbol] the type to check
|
|
@@ -140,6 +152,7 @@ module Purl
|
|
|
140
152
|
examples: type_examples(normalized_type),
|
|
141
153
|
registry_url_generation: RegistryURL.supports?(normalized_type),
|
|
142
154
|
reverse_parsing: RegistryURL.supported_reverse_types.include?(normalized_type),
|
|
155
|
+
download_url_generation: DownloadURL.supports?(normalized_type),
|
|
143
156
|
route_patterns: RegistryURL.route_patterns_for(normalized_type)
|
|
144
157
|
}
|
|
145
158
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: purl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Nesbitt
|
|
@@ -45,6 +45,7 @@ files:
|
|
|
45
45
|
- lib/purl.rb
|
|
46
46
|
- lib/purl/advisory.rb
|
|
47
47
|
- lib/purl/advisory_formatter.rb
|
|
48
|
+
- lib/purl/download_url.rb
|
|
48
49
|
- lib/purl/errors.rb
|
|
49
50
|
- lib/purl/lookup.rb
|
|
50
51
|
- lib/purl/lookup_formatter.rb
|
|
@@ -74,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
74
75
|
- !ruby/object:Gem::Version
|
|
75
76
|
version: '0'
|
|
76
77
|
requirements: []
|
|
77
|
-
rubygems_version:
|
|
78
|
+
rubygems_version: 4.0.1
|
|
78
79
|
specification_version: 4
|
|
79
80
|
summary: Parse and convert package urls (purls)
|
|
80
81
|
test_files: []
|