packageurl-ruby 0.1.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 +7 -0
- data/.github/workflows/ci.yml +33 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +22 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +100 -0
- data/README.md +67 -0
- data/Rakefile +14 -0
- data/Steepfile +22 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/package_url/version.rb +6 -0
- data/lib/package_url.rb +358 -0
- data/packageurl-ruby.gemspec +40 -0
- data/sig/package_url.rbs +44 -0
- metadata +64 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e48cdeff19115a2dbca64c93e81d7cd4de3a45946927157eeba40388de8ec04f
|
|
4
|
+
data.tar.gz: e94e269df648bade8528f547cfaca06c7e29d8fa23b9f318122d313665da796f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 92f391f396e54dd47e408cac234d5e4982d5ec91418d0ddb1198d07bc7d58cbea59e7323221f973f54bed72ce78e351c861bdd075ff83940d17cb5290da68fc3
|
|
7
|
+
data.tar.gz: 4d8e28fbfad4fed559ac1592ba5dc1d38b76f653508409b209b9febd03b5927bef281a36aa830945971bc75eb49a2cf1bc2e45ec2413c2b5444bb8e2f0eca446
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
ruby-version: ["2.7", "3.0"]
|
|
15
|
+
|
|
16
|
+
env:
|
|
17
|
+
RUBYOPT: "-W:no-experimental"
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v2
|
|
21
|
+
- name: Set up Ruby
|
|
22
|
+
uses: ruby/setup-ruby@v1
|
|
23
|
+
with:
|
|
24
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
25
|
+
bundler-cache: true
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: bundle exec rspec
|
|
28
|
+
- name: Perform type check
|
|
29
|
+
run: bundle exec steep check
|
|
30
|
+
- name: Lint
|
|
31
|
+
run: bundle exec rubocop
|
|
32
|
+
- name: Check documentation coverage
|
|
33
|
+
run: bundle exec yard stats --list-undoc
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 2.7
|
|
3
|
+
NewCops: enable
|
|
4
|
+
Metrics/AbcSize:
|
|
5
|
+
Enabled: false
|
|
6
|
+
Metrics/BlockLength:
|
|
7
|
+
Exclude:
|
|
8
|
+
- "Rakefile"
|
|
9
|
+
- "**/*.rake"
|
|
10
|
+
- "spec/**/*.rb"
|
|
11
|
+
Metrics/CyclomaticComplexity:
|
|
12
|
+
Enabled: false
|
|
13
|
+
Metrics/ClassLength:
|
|
14
|
+
Max: 500
|
|
15
|
+
Metrics/MethodLength:
|
|
16
|
+
Max: 100
|
|
17
|
+
Metrics/ParameterLists:
|
|
18
|
+
Max: 7
|
|
19
|
+
Metrics/PerceivedComplexity:
|
|
20
|
+
Max: 15
|
|
21
|
+
Style/ConditionalAssignment:
|
|
22
|
+
Enabled: false
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.0.1
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [v0.0.1] - 2021-12-09
|
|
11
|
+
|
|
12
|
+
Initial release.
|
|
13
|
+
|
|
14
|
+
[unreleased]: https://github.com/package-url/packageurl-ruby/releases/tag/v0.1.0...main
|
|
15
|
+
[v0.1.0]: https://github.com/package-url/packageurl-ruby/releases/tag/v0.1.0
|
data/Gemfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
|
|
5
|
+
gemspec
|
|
6
|
+
|
|
7
|
+
gem 'bundler', '~> 2.0'
|
|
8
|
+
gem 'rake', '~> 13.0'
|
|
9
|
+
gem 'rspec', '~> 3.0'
|
|
10
|
+
gem 'rubocop', '~> 1.23.0'
|
|
11
|
+
gem 'rubocop-rake', '~> 0.6.0'
|
|
12
|
+
gem 'rubocop-rspec', '~> 2.6.0'
|
|
13
|
+
gem 'steep', '~> 0.46.0'
|
|
14
|
+
gem 'yard', '~> 0.9.0'
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
packageurl-ruby (0.1.0)
|
|
5
|
+
|
|
6
|
+
GEM
|
|
7
|
+
remote: https://rubygems.org/
|
|
8
|
+
specs:
|
|
9
|
+
activesupport (6.1.4.1)
|
|
10
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
11
|
+
i18n (>= 1.6, < 2)
|
|
12
|
+
minitest (>= 5.1)
|
|
13
|
+
tzinfo (~> 2.0)
|
|
14
|
+
zeitwerk (~> 2.3)
|
|
15
|
+
ast (2.4.2)
|
|
16
|
+
concurrent-ruby (1.1.9)
|
|
17
|
+
diff-lcs (1.4.4)
|
|
18
|
+
ffi (1.15.4)
|
|
19
|
+
i18n (1.8.11)
|
|
20
|
+
concurrent-ruby (~> 1.0)
|
|
21
|
+
language_server-protocol (3.16.0.3)
|
|
22
|
+
listen (3.7.0)
|
|
23
|
+
rb-fsevent (~> 0.10, >= 0.10.3)
|
|
24
|
+
rb-inotify (~> 0.9, >= 0.9.10)
|
|
25
|
+
minitest (5.14.4)
|
|
26
|
+
parallel (1.21.0)
|
|
27
|
+
parser (3.0.3.0)
|
|
28
|
+
ast (~> 2.4.1)
|
|
29
|
+
rainbow (3.0.0)
|
|
30
|
+
rake (13.0.6)
|
|
31
|
+
rb-fsevent (0.11.0)
|
|
32
|
+
rb-inotify (0.10.1)
|
|
33
|
+
ffi (~> 1.0)
|
|
34
|
+
rbs (1.7.1)
|
|
35
|
+
regexp_parser (2.1.1)
|
|
36
|
+
rexml (3.2.5)
|
|
37
|
+
rspec (3.10.0)
|
|
38
|
+
rspec-core (~> 3.10.0)
|
|
39
|
+
rspec-expectations (~> 3.10.0)
|
|
40
|
+
rspec-mocks (~> 3.10.0)
|
|
41
|
+
rspec-core (3.10.1)
|
|
42
|
+
rspec-support (~> 3.10.0)
|
|
43
|
+
rspec-expectations (3.10.1)
|
|
44
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
45
|
+
rspec-support (~> 3.10.0)
|
|
46
|
+
rspec-mocks (3.10.2)
|
|
47
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
48
|
+
rspec-support (~> 3.10.0)
|
|
49
|
+
rspec-support (3.10.3)
|
|
50
|
+
rubocop (1.23.0)
|
|
51
|
+
parallel (~> 1.10)
|
|
52
|
+
parser (>= 3.0.0.0)
|
|
53
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
54
|
+
regexp_parser (>= 1.8, < 3.0)
|
|
55
|
+
rexml
|
|
56
|
+
rubocop-ast (>= 1.12.0, < 2.0)
|
|
57
|
+
ruby-progressbar (~> 1.7)
|
|
58
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
|
59
|
+
rubocop-ast (1.13.0)
|
|
60
|
+
parser (>= 3.0.1.1)
|
|
61
|
+
rubocop-rake (0.6.0)
|
|
62
|
+
rubocop (~> 1.0)
|
|
63
|
+
rubocop-rspec (2.6.0)
|
|
64
|
+
rubocop (~> 1.19)
|
|
65
|
+
ruby-progressbar (1.11.0)
|
|
66
|
+
steep (0.46.0)
|
|
67
|
+
activesupport (>= 5.1)
|
|
68
|
+
language_server-protocol (>= 3.15, < 4.0)
|
|
69
|
+
listen (~> 3.0)
|
|
70
|
+
parallel (>= 1.0.0)
|
|
71
|
+
parser (>= 3.0)
|
|
72
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
73
|
+
rbs (>= 1.2.0)
|
|
74
|
+
terminal-table (>= 2, < 4)
|
|
75
|
+
terminal-table (3.0.2)
|
|
76
|
+
unicode-display_width (>= 1.1.1, < 3)
|
|
77
|
+
tzinfo (2.0.4)
|
|
78
|
+
concurrent-ruby (~> 1.0)
|
|
79
|
+
unicode-display_width (2.1.0)
|
|
80
|
+
yard (0.9.26)
|
|
81
|
+
zeitwerk (2.5.1)
|
|
82
|
+
|
|
83
|
+
PLATFORMS
|
|
84
|
+
x86_64-darwin-19
|
|
85
|
+
x86_64-darwin-20
|
|
86
|
+
x86_64-linux
|
|
87
|
+
|
|
88
|
+
DEPENDENCIES
|
|
89
|
+
bundler (~> 2.0)
|
|
90
|
+
packageurl-ruby!
|
|
91
|
+
rake (~> 13.0)
|
|
92
|
+
rspec (~> 3.0)
|
|
93
|
+
rubocop (~> 1.23.0)
|
|
94
|
+
rubocop-rake (~> 0.6.0)
|
|
95
|
+
rubocop-rspec (~> 2.6.0)
|
|
96
|
+
steep (~> 0.46.0)
|
|
97
|
+
yard (~> 0.9.0)
|
|
98
|
+
|
|
99
|
+
BUNDLED WITH
|
|
100
|
+
2.2.32
|
data/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# packageurl-ruby
|
|
2
|
+
|
|
3
|
+
![CI][ci badge]
|
|
4
|
+
|
|
5
|
+
A Ruby implementation of the [package url specification][purl-spec].
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Ruby 2.7+
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Add this line to your application's Gemfile:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem 'packageurl-ruby'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
And then execute:
|
|
20
|
+
|
|
21
|
+
```console
|
|
22
|
+
$ bundle install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or install it yourself as:
|
|
26
|
+
|
|
27
|
+
```console
|
|
28
|
+
$ gem install packageurl-ruby
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
require 'packageurl-ruby'
|
|
35
|
+
|
|
36
|
+
purl = PackageURL.parse("pkg:gem/rails@6.1.4")
|
|
37
|
+
purl.type # "gem"
|
|
38
|
+
purl.name # "rails"
|
|
39
|
+
purl.version # "6.1.4"
|
|
40
|
+
|
|
41
|
+
# supports pattern matching with hashes and arrays
|
|
42
|
+
case purl
|
|
43
|
+
in type: 'gem', name: 'rails'
|
|
44
|
+
puts 'Yay! You’re on Rails!'
|
|
45
|
+
in ['pkg', 'gem', *]
|
|
46
|
+
puts '🦊🗯 "Ruby is easy to read"'
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
|
53
|
+
Then, run `rake spec` to run the tests.
|
|
54
|
+
You can also run `bin/console` for an interactive prompt
|
|
55
|
+
that will allow you to experiment.
|
|
56
|
+
|
|
57
|
+
To install this gem onto your local machine,
|
|
58
|
+
run `bundle exec rake install`.
|
|
59
|
+
To release a new version,
|
|
60
|
+
update the version number in `version.rb`,
|
|
61
|
+
and then run `bundle exec rake release`,
|
|
62
|
+
which will create a git tag for the version,
|
|
63
|
+
push git commits and the created tag,
|
|
64
|
+
and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
65
|
+
|
|
66
|
+
[ci badge]: https://github.com/mattt/packageurl-ruby/workflows/CI/badge.svg
|
|
67
|
+
[purl-spec]: https://github.com/package-url/purl-spec
|
data/Rakefile
ADDED
data/Steepfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
target :lib do
|
|
4
|
+
signature 'sig'
|
|
5
|
+
|
|
6
|
+
check 'lib'
|
|
7
|
+
|
|
8
|
+
library 'uri'
|
|
9
|
+
|
|
10
|
+
configure_code_diagnostics do |config|
|
|
11
|
+
config[Steep::Diagnostic::Ruby::UnsupportedSyntax] = :hint
|
|
12
|
+
config[Steep::Diagnostic::Ruby::MethodDefinitionMissing] = :hint
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
target :test do
|
|
17
|
+
signature 'sig'
|
|
18
|
+
|
|
19
|
+
check 'test'
|
|
20
|
+
|
|
21
|
+
library 'uri'
|
|
22
|
+
end
|
data/bin/console
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
require 'package_url'
|
|
6
|
+
|
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
+
|
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
11
|
+
# require "pry"
|
|
12
|
+
# Pry.start
|
|
13
|
+
|
|
14
|
+
require 'irb'
|
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/package_url.rb
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'package_url/version'
|
|
4
|
+
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
# A package URL, or _purl_, is a URL string used to
|
|
8
|
+
# identify and locate a software package in a mostly universal and uniform way
|
|
9
|
+
# across programing languages, package managers, packaging conventions, tools,
|
|
10
|
+
# APIs and databases.
|
|
11
|
+
#
|
|
12
|
+
# A purl is a URL composed of seven components:
|
|
13
|
+
#
|
|
14
|
+
# ```
|
|
15
|
+
# scheme:type/namespace/name@version?qualifiers#subpath
|
|
16
|
+
# ```
|
|
17
|
+
#
|
|
18
|
+
# For example,
|
|
19
|
+
# the package URL for this Ruby package at version 0.1.0 is
|
|
20
|
+
# `pkg:ruby/mattt/packageurl-ruby@0.1.0`.
|
|
21
|
+
class PackageURL
|
|
22
|
+
# Raised when attempting to parse an invalid package URL string.
|
|
23
|
+
# @see #parse
|
|
24
|
+
class InvalidPackageURL < ArgumentError; end
|
|
25
|
+
|
|
26
|
+
# The URL scheme, which has a constant value of `"pkg"`.
|
|
27
|
+
def scheme
|
|
28
|
+
'pkg'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The package type or protocol, such as `"gem"`, `"npm"`, and `"github"`.
|
|
32
|
+
attr_reader :type
|
|
33
|
+
|
|
34
|
+
# A name prefix, specific to the type of package.
|
|
35
|
+
# For example, an npm scope, a Docker image owner, or a GitHub user.
|
|
36
|
+
attr_reader :namespace
|
|
37
|
+
|
|
38
|
+
# The name of the package.
|
|
39
|
+
attr_reader :name
|
|
40
|
+
|
|
41
|
+
# The version of the package.
|
|
42
|
+
attr_reader :version
|
|
43
|
+
|
|
44
|
+
# Extra qualifying data for a package, specific to the type of package.
|
|
45
|
+
# For example, the operating system or architecture.
|
|
46
|
+
attr_reader :qualifiers
|
|
47
|
+
|
|
48
|
+
# An extra subpath within a package, relative to the package root.
|
|
49
|
+
attr_reader :subpath
|
|
50
|
+
|
|
51
|
+
# Constructs a package URL from its components
|
|
52
|
+
# @param type [String] The package type or protocol.
|
|
53
|
+
# @param namespace [String] A name prefix, specific to the type of package.
|
|
54
|
+
# @param name [String] The name of the package.
|
|
55
|
+
# @param version [String] The version of the package.
|
|
56
|
+
# @param qualifiers [Hash] Extra qualifying data for a package, specific to the type of package.
|
|
57
|
+
# @param subpath [String] An extra subpath within a package, relative to the package root.
|
|
58
|
+
def initialize(type:, name:, namespace: nil, version: nil, qualifiers: nil, subpath: nil)
|
|
59
|
+
raise ArgumentError, 'type is required' if type.nil? || type.empty?
|
|
60
|
+
raise ArgumentError, 'name is required' if name.nil? || name.empty?
|
|
61
|
+
|
|
62
|
+
@type = type.downcase
|
|
63
|
+
@namespace = namespace
|
|
64
|
+
@name = name
|
|
65
|
+
@version = version
|
|
66
|
+
@qualifiers = qualifiers
|
|
67
|
+
@subpath = subpath
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Creates a new PackageURL from a string.
|
|
71
|
+
# @param [String] string The package URL string.
|
|
72
|
+
# @raise [InvalidPackageURL] If the string is not a valid package URL.
|
|
73
|
+
# @return [PackageURL]
|
|
74
|
+
def self.parse(string)
|
|
75
|
+
components = {}
|
|
76
|
+
|
|
77
|
+
# Split the purl string once from right on '#'
|
|
78
|
+
# - The left side is the remainder
|
|
79
|
+
# - Strip the right side from leading and trailing '/'
|
|
80
|
+
# - Split this on '/'
|
|
81
|
+
# - Discard any empty string segment from that split
|
|
82
|
+
# - Discard any '.' or '..' segment from that split
|
|
83
|
+
# - Percent-decode each segment
|
|
84
|
+
# - UTF-8-decode each segment if needed in your programming language
|
|
85
|
+
# - Join segments back with a '/'
|
|
86
|
+
# - This is the subpath
|
|
87
|
+
case string.rpartition('#')
|
|
88
|
+
in String => remainder, separator, String => subpath unless separator.empty?
|
|
89
|
+
components[:subpath] = subpath.split('/').select do |segment|
|
|
90
|
+
!segment.empty? && segment != '.' && segment != '..'
|
|
91
|
+
end.compact.join('/')
|
|
92
|
+
|
|
93
|
+
string = remainder
|
|
94
|
+
else
|
|
95
|
+
components[:subpath] = nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Split the remainder once from right on '?'
|
|
99
|
+
# - The left side is the remainder
|
|
100
|
+
# - The right side is the qualifiers string
|
|
101
|
+
# - Split the qualifiers on '&'. Each part is a key=value pair
|
|
102
|
+
# - For each pair, split the key=value once from left on '=':
|
|
103
|
+
# - The key is the lowercase left side
|
|
104
|
+
# - The value is the percent-decoded right side
|
|
105
|
+
# - UTF-8-decode the value if needed in your programming language
|
|
106
|
+
# - Discard any key/value pairs where the value is empty
|
|
107
|
+
# - If the key is checksums,
|
|
108
|
+
# split the value on ',' to create a list of checksums
|
|
109
|
+
# - This list of key/value is the qualifiers object
|
|
110
|
+
case string.rpartition('?')
|
|
111
|
+
in String => remainder, separator, String => qualifiers unless separator.empty?
|
|
112
|
+
components[:qualifiers] = {}
|
|
113
|
+
|
|
114
|
+
qualifiers.split('&').each do |pair|
|
|
115
|
+
case pair.partition('=')
|
|
116
|
+
in String => key, separator, String => value unless separator.empty?
|
|
117
|
+
key = key.downcase
|
|
118
|
+
value = URI.decode_www_form_component(value)
|
|
119
|
+
next if value.empty?
|
|
120
|
+
|
|
121
|
+
case key
|
|
122
|
+
when 'checksums'
|
|
123
|
+
components[:qualifiers][key] = value.split(',')
|
|
124
|
+
else
|
|
125
|
+
components[:qualifiers][key] = value
|
|
126
|
+
end
|
|
127
|
+
else
|
|
128
|
+
next
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
string = remainder
|
|
133
|
+
else
|
|
134
|
+
components[:qualifiers] = nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Split the remainder once from left on ':'
|
|
138
|
+
# - The left side lowercased is the scheme
|
|
139
|
+
# - The right side is the remainder
|
|
140
|
+
case string.partition(':')
|
|
141
|
+
in 'pkg', separator, String => remainder unless separator.empty?
|
|
142
|
+
string = remainder
|
|
143
|
+
else
|
|
144
|
+
raise InvalidPackageURL, 'invalid or missing "pkg:" URL scheme'
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Strip the remainder from leading and trailing '/'
|
|
148
|
+
# - Split this once from left on '/'
|
|
149
|
+
# - The left side lowercased is the type
|
|
150
|
+
# - The right side is the remainder
|
|
151
|
+
string = string.delete_suffix('/')
|
|
152
|
+
case string.partition('/')
|
|
153
|
+
in String => type, separator, remainder unless separator.empty?
|
|
154
|
+
components[:type] = type
|
|
155
|
+
|
|
156
|
+
string = remainder
|
|
157
|
+
else
|
|
158
|
+
raise InvalidPackageURL, 'invalid or missing package type'
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Split the remainder once from right on '@'
|
|
162
|
+
# - The left side is the remainder
|
|
163
|
+
# - Percent-decode the right side. This is the version.
|
|
164
|
+
# - UTF-8-decode the version if needed in your programming language
|
|
165
|
+
# - This is the version
|
|
166
|
+
case string.rpartition('@')
|
|
167
|
+
in String => remainder, separator, String => version unless separator.empty?
|
|
168
|
+
components[:version] = URI.decode_www_form_component(version)
|
|
169
|
+
|
|
170
|
+
string = remainder
|
|
171
|
+
else
|
|
172
|
+
components[:version] = nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Split the remainder once from right on '/'
|
|
176
|
+
# - The left side is the remainder
|
|
177
|
+
# - Percent-decode the right side. This is the name
|
|
178
|
+
# - UTF-8-decode this name if needed in your programming language
|
|
179
|
+
# - Apply type-specific normalization to the name if needed
|
|
180
|
+
# - This is the name
|
|
181
|
+
case string.rpartition('/')
|
|
182
|
+
in String => remainder, separator, String => name unless separator.empty?
|
|
183
|
+
components[:name] = URI.decode_www_form_component(name)
|
|
184
|
+
|
|
185
|
+
# Split the remainder on '/'
|
|
186
|
+
# - Discard any empty segment from that split
|
|
187
|
+
# - Percent-decode each segment
|
|
188
|
+
# - UTF-8-decode the each segment if needed in your programming language
|
|
189
|
+
# - Apply type-specific normalization to each segment if needed
|
|
190
|
+
# - Join segments back with a '/'
|
|
191
|
+
# - This is the namespace
|
|
192
|
+
components[:namespace] = remainder.split('/').map { |s| URI.decode_www_form_component(s) }.compact.join('/')
|
|
193
|
+
in _, _, String => name
|
|
194
|
+
components[:name] = URI.decode_www_form_component(name)
|
|
195
|
+
components[:namespace] = nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
new(type: components[:type],
|
|
199
|
+
name: components[:name],
|
|
200
|
+
namespace: components[:namespace],
|
|
201
|
+
version: components[:version],
|
|
202
|
+
qualifiers: components[:qualifiers],
|
|
203
|
+
subpath: components[:subpath])
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Returns a hash containing the
|
|
207
|
+
# scheme, type, namespace, name, version, qualifiers, and subpath components
|
|
208
|
+
# of the package URL.
|
|
209
|
+
def to_h
|
|
210
|
+
{
|
|
211
|
+
scheme: scheme,
|
|
212
|
+
type: @type,
|
|
213
|
+
namespace: @namespace,
|
|
214
|
+
name: @name,
|
|
215
|
+
version: @version,
|
|
216
|
+
qualifiers: @qualifiers,
|
|
217
|
+
subpath: @subpath
|
|
218
|
+
}
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Returns a string representation of the package URL.
|
|
222
|
+
# Package URL representations are created according to the instructions from
|
|
223
|
+
# https://github.com/package-url/purl-spec/blob/0b1559f76b79829e789c4f20e6d832c7314762c5/PURL-SPECIFICATION.rst#how-to-build-purl-string-from-its-components.
|
|
224
|
+
def to_s
|
|
225
|
+
# Start a purl string with the "pkg:" scheme as a lowercase ASCII string
|
|
226
|
+
purl = 'pkg:'
|
|
227
|
+
|
|
228
|
+
# Append the type string to the purl as a lowercase ASCII string
|
|
229
|
+
# Append '/' to the purl
|
|
230
|
+
|
|
231
|
+
purl += @type
|
|
232
|
+
purl += '/'
|
|
233
|
+
|
|
234
|
+
# If the namespace is not empty:
|
|
235
|
+
# - Strip the namespace from leading and trailing '/'
|
|
236
|
+
# - Split on '/' as segments
|
|
237
|
+
# - Apply type-specific normalization to each segment if needed
|
|
238
|
+
# - UTF-8-encode each segment if needed in your programming language
|
|
239
|
+
# - Percent-encode each segment
|
|
240
|
+
# - Join the segments with '/'
|
|
241
|
+
# - Append this to the purl
|
|
242
|
+
# - Append '/' to the purl
|
|
243
|
+
# - Strip the name from leading and trailing '/'
|
|
244
|
+
# - Apply type-specific normalization to the name if needed
|
|
245
|
+
# - UTF-8-encode the name if needed in your programming language
|
|
246
|
+
# - Append the percent-encoded name to the purl
|
|
247
|
+
#
|
|
248
|
+
# If the namespace is empty:
|
|
249
|
+
# - Apply type-specific normalization to the name if needed
|
|
250
|
+
# - UTF-8-encode the name if needed in your programming language
|
|
251
|
+
# - Append the percent-encoded name to the purl
|
|
252
|
+
case @namespace
|
|
253
|
+
in String => namespace unless namespace.empty?
|
|
254
|
+
segments = []
|
|
255
|
+
@namespace.delete_prefix('/').delete_suffix('/').split('/').each do |segment|
|
|
256
|
+
next if segment.empty?
|
|
257
|
+
|
|
258
|
+
segments << URI.encode_www_form_component(segment)
|
|
259
|
+
end
|
|
260
|
+
purl += segments.join('/')
|
|
261
|
+
|
|
262
|
+
purl += '/'
|
|
263
|
+
purl += URI.encode_www_form_component(@name.delete_prefix('/').delete_suffix('/'))
|
|
264
|
+
else
|
|
265
|
+
purl += URI.encode_www_form_component(@name)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# If the version is not empty:
|
|
269
|
+
# - Append '@' to the purl
|
|
270
|
+
# - UTF-8-encode the version if needed in your programming language
|
|
271
|
+
# - Append the percent-encoded version to the purl
|
|
272
|
+
case @version
|
|
273
|
+
in String => version unless version.empty?
|
|
274
|
+
purl += '@'
|
|
275
|
+
purl += URI.encode_www_form_component(@version)
|
|
276
|
+
else
|
|
277
|
+
nil
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# If the qualifiers are not empty and not composed only of key/value pairs
|
|
281
|
+
# where the value is empty:
|
|
282
|
+
# - Append '?' to the purl
|
|
283
|
+
# - Build a list from all key/value pair:
|
|
284
|
+
# - discard any pair where the value is empty.
|
|
285
|
+
# - UTF-8-encode each value if needed in your programming language
|
|
286
|
+
# - If the key is checksums and this is a list of checksums
|
|
287
|
+
# join this list with a ',' to create this qualifier value
|
|
288
|
+
# - create a string by joining the lowercased key,
|
|
289
|
+
# the equal '=' sign and the percent-encoded value to create a qualifier
|
|
290
|
+
# - sort this list of qualifier strings lexicographically
|
|
291
|
+
# - join this list of qualifier strings with a '&' ampersand
|
|
292
|
+
# - Append this string to the purl
|
|
293
|
+
case @qualifiers
|
|
294
|
+
in Hash => qualifiers unless qualifiers.empty?
|
|
295
|
+
list = []
|
|
296
|
+
qualifiers.each do |key, value|
|
|
297
|
+
next if value.empty?
|
|
298
|
+
|
|
299
|
+
case [key, value]
|
|
300
|
+
in 'checksums', Array => checksums
|
|
301
|
+
list << "#{key.downcase}=#{checksums.join(',')}"
|
|
302
|
+
else
|
|
303
|
+
list << "#{key.downcase}=#{URI.encode_www_form_component(value)}"
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
unless list.empty?
|
|
308
|
+
purl += '?'
|
|
309
|
+
purl += list.sort.join('&')
|
|
310
|
+
end
|
|
311
|
+
else
|
|
312
|
+
nil
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# If the subpath is not empty and not composed only of
|
|
316
|
+
# empty, '.' and '..' segments:
|
|
317
|
+
# - Append '#' to the purl
|
|
318
|
+
# - Strip the subpath from leading and trailing '/'
|
|
319
|
+
# - Split this on '/' as segments
|
|
320
|
+
# - Discard empty, '.' and '..' segments
|
|
321
|
+
# - Percent-encode each segment
|
|
322
|
+
# - UTF-8-encode each segment if needed in your programming language
|
|
323
|
+
# - Join the segments with '/'
|
|
324
|
+
# - Append this to the purl
|
|
325
|
+
case @subpath
|
|
326
|
+
in String => subpath unless subpath.empty?
|
|
327
|
+
segments = []
|
|
328
|
+
subpath.delete_prefix('/').delete_suffix('/').split('/').each do |segment|
|
|
329
|
+
next if segment.empty? || segment == '.' || segment == '..'
|
|
330
|
+
|
|
331
|
+
segments << URI.encode_www_form_component(segment)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
unless segments.empty?
|
|
335
|
+
purl += '#'
|
|
336
|
+
purl += segments.join('/')
|
|
337
|
+
end
|
|
338
|
+
else
|
|
339
|
+
nil
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
purl
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Returns an array containing the
|
|
346
|
+
# scheme, type, namespace, name, version, qualifiers, and subpath components
|
|
347
|
+
# of the package URL.
|
|
348
|
+
def deconstruct
|
|
349
|
+
[scheme, @type, @namespace, @name, @version, @qualifiers, @subpath]
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Returns a hash containing the
|
|
353
|
+
# scheme, type, namespace, name, version, qualifiers, and subpath components
|
|
354
|
+
# of the package URL.
|
|
355
|
+
def deconstruct_keys(_keys)
|
|
356
|
+
to_h
|
|
357
|
+
end
|
|
358
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/package_url/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'packageurl-ruby'
|
|
7
|
+
spec.version = PackageURL::VERSION
|
|
8
|
+
spec.authors = ['Mattt']
|
|
9
|
+
spec.email = ['mattt@me.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Ruby implementation of the package url spec'
|
|
12
|
+
spec.description = <<-DESCRIPTION
|
|
13
|
+
A package URL, or purl, is a URL string used to
|
|
14
|
+
identify and locate a software package in a mostly universal and uniform way
|
|
15
|
+
across programing languages, package managers, packaging conventions,
|
|
16
|
+
tools, APIs and databases.
|
|
17
|
+
DESCRIPTION
|
|
18
|
+
|
|
19
|
+
spec.homepage = 'https://github.com/package-url/packageurl-ruby'
|
|
20
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
|
|
21
|
+
|
|
22
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
23
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
24
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
25
|
+
|
|
26
|
+
# Specify which files should be added to the gem when it is released.
|
|
27
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
28
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
29
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
|
30
|
+
end
|
|
31
|
+
spec.bindir = 'exe'
|
|
32
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
33
|
+
spec.require_paths = ['lib']
|
|
34
|
+
|
|
35
|
+
# For more information and examples about making a new gem, checkout our
|
|
36
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
37
|
+
spec.metadata = {
|
|
38
|
+
'rubygems_mfa_required' => 'true'
|
|
39
|
+
}
|
|
40
|
+
end
|
data/sig/package_url.rbs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# A package URL, or _purl_, is a URL string used to identify and locate a software package
|
|
2
|
+
# in a mostly universal and uniform way across
|
|
3
|
+
# programing languages, package managers, packaging conventions, tools, APIs and databases.
|
|
4
|
+
#
|
|
5
|
+
# A purl is a URL composed of seven components:
|
|
6
|
+
#
|
|
7
|
+
# ```
|
|
8
|
+
# scheme:type/namespace/name@version?qualifiers#subpath
|
|
9
|
+
# ```
|
|
10
|
+
#
|
|
11
|
+
# For example,
|
|
12
|
+
# the package URL for this Ruby package at version 0.1.0 is
|
|
13
|
+
# `pkg:ruby/mattt/packageurl-ruby@0.1.0`.
|
|
14
|
+
class PackageURL
|
|
15
|
+
VERSION: String
|
|
16
|
+
|
|
17
|
+
def scheme: () -> String
|
|
18
|
+
attr_reader type: String
|
|
19
|
+
attr_reader namespace: String?
|
|
20
|
+
attr_reader name: String?
|
|
21
|
+
attr_reader version: String?
|
|
22
|
+
attr_reader qualifiers: Hash[String, String]?
|
|
23
|
+
attr_reader subpath: String?
|
|
24
|
+
|
|
25
|
+
def initialize: (type: String `type`,
|
|
26
|
+
?namespace: String? namespace,
|
|
27
|
+
name: String name,
|
|
28
|
+
?version: String? version,
|
|
29
|
+
?qualifiers: Hash[String, String]? qualifiers,
|
|
30
|
+
?subpath: String? subpath) -> void
|
|
31
|
+
|
|
32
|
+
def self.parse: (String string) -> PackageURL?
|
|
33
|
+
|
|
34
|
+
def to_h: () -> { scheme: String, type: String, namespace: String?, name: String?, version: String?, qualifiers: Hash[String, String]?, subpath: String? }
|
|
35
|
+
|
|
36
|
+
# Returns a string representation of the package URL.
|
|
37
|
+
# Package URL representations are created according to the instructions provided at
|
|
38
|
+
# https://github.com/package-url/purl-spec/blob/0b1559f76b79829e789c4f20e6d832c7314762c5/PURL-SPECIFICATION.rst#how-to-build-purl-string-from-its-components.
|
|
39
|
+
def to_s: () -> String
|
|
40
|
+
|
|
41
|
+
def deconstruct: () -> Array[String | Hash[String, String] | nil]
|
|
42
|
+
|
|
43
|
+
def deconstruct_keys: (Array[Symbol] keys) -> { scheme: String, type: String, namespace: String?, name: String?, version: String?, qualifiers: Hash[String, String]?, subpath: String? }
|
|
44
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: packageurl-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mattt
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2021-12-10 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |2
|
|
14
|
+
A package URL, or purl, is a URL string used to
|
|
15
|
+
identify and locate a software package in a mostly universal and uniform way
|
|
16
|
+
across programing languages, package managers, packaging conventions,
|
|
17
|
+
tools, APIs and databases.
|
|
18
|
+
email:
|
|
19
|
+
- mattt@me.com
|
|
20
|
+
executables: []
|
|
21
|
+
extensions: []
|
|
22
|
+
extra_rdoc_files: []
|
|
23
|
+
files:
|
|
24
|
+
- ".github/workflows/ci.yml"
|
|
25
|
+
- ".gitignore"
|
|
26
|
+
- ".rspec"
|
|
27
|
+
- ".rubocop.yml"
|
|
28
|
+
- ".ruby-version"
|
|
29
|
+
- CHANGELOG.md
|
|
30
|
+
- Gemfile
|
|
31
|
+
- Gemfile.lock
|
|
32
|
+
- README.md
|
|
33
|
+
- Rakefile
|
|
34
|
+
- Steepfile
|
|
35
|
+
- bin/console
|
|
36
|
+
- bin/setup
|
|
37
|
+
- lib/package_url.rb
|
|
38
|
+
- lib/package_url/version.rb
|
|
39
|
+
- packageurl-ruby.gemspec
|
|
40
|
+
- sig/package_url.rbs
|
|
41
|
+
homepage: https://github.com/package-url/packageurl-ruby
|
|
42
|
+
licenses: []
|
|
43
|
+
metadata:
|
|
44
|
+
rubygems_mfa_required: 'true'
|
|
45
|
+
post_install_message:
|
|
46
|
+
rdoc_options: []
|
|
47
|
+
require_paths:
|
|
48
|
+
- lib
|
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 2.7.0
|
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
requirements: []
|
|
60
|
+
rubygems_version: 3.2.15
|
|
61
|
+
signing_key:
|
|
62
|
+
specification_version: 4
|
|
63
|
+
summary: Ruby implementation of the package url spec
|
|
64
|
+
test_files: []
|