Class: PackageURL
- Inherits:
-
Object
- Object
- PackageURL
- Defined in:
- lib/package_url.rb,
lib/package_url/version.rb
Overview
A package URL, or purl, is a URL string used to identify and locate a software package in a mostly universal and uniform way across programing languages, package managers, packaging conventions, tools, APIs and databases.
A purl is a URL composed of seven components:
“‘ scheme:type/namespace/name@version?qualifiers#subpath “`
For example, the package URL for this Ruby package at version 0.1.0 is ‘pkg:ruby/mattt/[email protected]`.
Defined Under Namespace
Classes: InvalidPackageURL
Constant Summary collapse
- VERSION =
:nodoc:
'0.1.0'
Instance Attribute Summary collapse
-
#name ⇒ Object
readonly
The name of the package.
-
#namespace ⇒ Object
readonly
A name prefix, specific to the type of package.
-
#qualifiers ⇒ Object
readonly
Extra qualifying data for a package, specific to the type of package.
-
#subpath ⇒ Object
readonly
An extra subpath within a package, relative to the package root.
-
#type ⇒ Object
readonly
The package type or protocol, such as ‘“gem”`, `“npm”`, and `“github”`.
-
#version ⇒ Object
readonly
The version of the package.
Class Method Summary collapse
-
.parse(string) ⇒ PackageURL
Creates a new PackageURL from a string.
Instance Method Summary collapse
-
#deconstruct ⇒ Object
Returns an array containing the scheme, type, namespace, name, version, qualifiers, and subpath components of the package URL.
-
#deconstruct_keys(_keys) ⇒ Object
Returns a hash containing the scheme, type, namespace, name, version, qualifiers, and subpath components of the package URL.
-
#initialize(type:, name:, namespace: nil, version: nil, qualifiers: nil, subpath: nil) ⇒ PackageURL
constructor
Constructs a package URL from its components.
-
#scheme ⇒ Object
The URL scheme, which has a constant value of ‘“pkg”`.
-
#to_h ⇒ Object
Returns a hash containing the scheme, type, namespace, name, version, qualifiers, and subpath components of the package URL.
-
#to_s ⇒ Object
Returns a string representation of the package URL.
Constructor Details
#initialize(type:, name:, namespace: nil, version: nil, qualifiers: nil, subpath: nil) ⇒ PackageURL
Constructs a package URL from its components
58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/package_url.rb', line 58 def initialize(type:, name:, namespace: nil, version: nil, qualifiers: nil, subpath: nil) raise ArgumentError, 'type is required' if type.nil? || type.empty? raise ArgumentError, 'name is required' if name.nil? || name.empty? @type = type.downcase @namespace = namespace @name = name @version = version @qualifiers = qualifiers @subpath = subpath end |
Instance Attribute Details
#name ⇒ Object (readonly)
The name of the package.
39 40 41 |
# File 'lib/package_url.rb', line 39 def name @name end |
#namespace ⇒ Object (readonly)
A name prefix, specific to the type of package. For example, an npm scope, a Docker image owner, or a GitHub user.
36 37 38 |
# File 'lib/package_url.rb', line 36 def namespace @namespace end |
#qualifiers ⇒ Object (readonly)
Extra qualifying data for a package, specific to the type of package. For example, the operating system or architecture.
46 47 48 |
# File 'lib/package_url.rb', line 46 def qualifiers @qualifiers end |
#subpath ⇒ Object (readonly)
An extra subpath within a package, relative to the package root.
49 50 51 |
# File 'lib/package_url.rb', line 49 def subpath @subpath end |
#type ⇒ Object (readonly)
The package type or protocol, such as ‘“gem”`, `“npm”`, and `“github”`.
32 33 34 |
# File 'lib/package_url.rb', line 32 def type @type end |
#version ⇒ Object (readonly)
The version of the package.
42 43 44 |
# File 'lib/package_url.rb', line 42 def version @version end |
Class Method Details
.parse(string) ⇒ PackageURL
Creates a new PackageURL from a string.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/package_url.rb', line 74 def self.parse(string) components = {} # Split the purl string once from right on '#' # - The left side is the remainder # - Strip the right side from leading and trailing '/' # - Split this on '/' # - Discard any empty string segment from that split # - Discard any '.' or '..' segment from that split # - Percent-decode each segment # - UTF-8-decode each segment if needed in your programming language # - Join segments back with a '/' # - This is the subpath case string.rpartition('#') in String => remainder, separator, String => subpath unless separator.empty? components[:subpath] = subpath.split('/').select do |segment| !segment.empty? && segment != '.' && segment != '..' end.compact.join('/') string = remainder else components[:subpath] = nil end # Split the remainder once from right on '?' # - The left side is the remainder # - The right side is the qualifiers string # - Split the qualifiers on '&'. Each part is a key=value pair # - For each pair, split the key=value once from left on '=': # - The key is the lowercase left side # - The value is the percent-decoded right side # - UTF-8-decode the value if needed in your programming language # - Discard any key/value pairs where the value is empty # - If the key is checksums, # split the value on ',' to create a list of checksums # - This list of key/value is the qualifiers object case string.rpartition('?') in String => remainder, separator, String => qualifiers unless separator.empty? components[:qualifiers] = {} qualifiers.split('&').each do |pair| case pair.partition('=') in String => key, separator, String => value unless separator.empty? key = key.downcase value = URI.decode_www_form_component(value) next if value.empty? case key when 'checksums' components[:qualifiers][key] = value.split(',') else components[:qualifiers][key] = value end else next end end string = remainder else components[:qualifiers] = nil end # Split the remainder once from left on ':' # - The left side lowercased is the scheme # - The right side is the remainder case string.partition(':') in 'pkg', separator, String => remainder unless separator.empty? string = remainder else raise InvalidPackageURL, 'invalid or missing "pkg:" URL scheme' end # Strip the remainder from leading and trailing '/' # - Split this once from left on '/' # - The left side lowercased is the type # - The right side is the remainder string = string.delete_suffix('/') case string.partition('/') in String => type, separator, remainder unless separator.empty? components[:type] = type string = remainder else raise InvalidPackageURL, 'invalid or missing package type' end # Split the remainder once from right on '@' # - The left side is the remainder # - Percent-decode the right side. This is the version. # - UTF-8-decode the version if needed in your programming language # - This is the version case string.rpartition('@') in String => remainder, separator, String => version unless separator.empty? components[:version] = URI.decode_www_form_component(version) string = remainder else components[:version] = nil end # Split the remainder once from right on '/' # - The left side is the remainder # - Percent-decode the right side. This is the name # - UTF-8-decode this name if needed in your programming language # - Apply type-specific normalization to the name if needed # - This is the name case string.rpartition('/') in String => remainder, separator, String => name unless separator.empty? components[:name] = URI.decode_www_form_component(name) # Split the remainder on '/' # - Discard any empty segment from that split # - Percent-decode each segment # - UTF-8-decode the each segment if needed in your programming language # - Apply type-specific normalization to each segment if needed # - Join segments back with a '/' # - This is the namespace components[:namespace] = remainder.split('/').map { |s| URI.decode_www_form_component(s) }.compact.join('/') in _, _, String => name components[:name] = URI.decode_www_form_component(name) components[:namespace] = nil end new(type: components[:type], name: components[:name], namespace: components[:namespace], version: components[:version], qualifiers: components[:qualifiers], subpath: components[:subpath]) end |
Instance Method Details
#deconstruct ⇒ Object
Returns an array containing the scheme, type, namespace, name, version, qualifiers, and subpath components of the package URL.
348 349 350 |
# File 'lib/package_url.rb', line 348 def deconstruct [scheme, @type, @namespace, @name, @version, @qualifiers, @subpath] end |
#deconstruct_keys(_keys) ⇒ Object
Returns a hash containing the scheme, type, namespace, name, version, qualifiers, and subpath components of the package URL.
355 356 357 |
# File 'lib/package_url.rb', line 355 def deconstruct_keys(_keys) to_h end |
#scheme ⇒ Object
The URL scheme, which has a constant value of ‘“pkg”`.
27 28 29 |
# File 'lib/package_url.rb', line 27 def scheme 'pkg' end |
#to_h ⇒ Object
Returns a hash containing the scheme, type, namespace, name, version, qualifiers, and subpath components of the package URL.
209 210 211 212 213 214 215 216 217 218 219 |
# File 'lib/package_url.rb', line 209 def to_h { scheme: scheme, type: @type, namespace: @namespace, name: @name, version: @version, qualifiers: @qualifiers, subpath: @subpath } end |
#to_s ⇒ Object
Returns a string representation of the package URL. Package URL representations are created according to the instructions from github.com/package-url/purl-spec/blob/0b1559f76b79829e789c4f20e6d832c7314762c5/PURL-SPECIFICATION.rst#how-to-build-purl-string-from-its-components.
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# File 'lib/package_url.rb', line 224 def to_s # Start a purl string with the "pkg:" scheme as a lowercase ASCII string purl = 'pkg:' # Append the type string to the purl as a lowercase ASCII string # Append '/' to the purl purl += @type purl += '/' # If the namespace is not empty: # - Strip the namespace from leading and trailing '/' # - Split on '/' as segments # - Apply type-specific normalization to each segment if needed # - UTF-8-encode each segment if needed in your programming language # - Percent-encode each segment # - Join the segments with '/' # - Append this to the purl # - Append '/' to the purl # - Strip the name from leading and trailing '/' # - Apply type-specific normalization to the name if needed # - UTF-8-encode the name if needed in your programming language # - Append the percent-encoded name to the purl # # If the namespace is empty: # - Apply type-specific normalization to the name if needed # - UTF-8-encode the name if needed in your programming language # - Append the percent-encoded name to the purl case @namespace in String => namespace unless namespace.empty? segments = [] @namespace.delete_prefix('/').delete_suffix('/').split('/').each do |segment| next if segment.empty? segments << URI.encode_www_form_component(segment) end purl += segments.join('/') purl += '/' purl += URI.encode_www_form_component(@name.delete_prefix('/').delete_suffix('/')) else purl += URI.encode_www_form_component(@name) end # If the version is not empty: # - Append '@' to the purl # - UTF-8-encode the version if needed in your programming language # - Append the percent-encoded version to the purl case @version in String => version unless version.empty? purl += '@' purl += URI.encode_www_form_component(@version) else nil end # If the qualifiers are not empty and not composed only of key/value pairs # where the value is empty: # - Append '?' to the purl # - Build a list from all key/value pair: # - discard any pair where the value is empty. # - UTF-8-encode each value if needed in your programming language # - If the key is checksums and this is a list of checksums # join this list with a ',' to create this qualifier value # - create a string by joining the lowercased key, # the equal '=' sign and the percent-encoded value to create a qualifier # - sort this list of qualifier strings lexicographically # - join this list of qualifier strings with a '&' ampersand # - Append this string to the purl case @qualifiers in Hash => qualifiers unless qualifiers.empty? list = [] qualifiers.each do |key, value| next if value.empty? case [key, value] in 'checksums', Array => checksums list << "#{key.downcase}=#{checksums.join(',')}" else list << "#{key.downcase}=#{URI.encode_www_form_component(value)}" end end unless list.empty? purl += '?' purl += list.sort.join('&') end else nil end # If the subpath is not empty and not composed only of # empty, '.' and '..' segments: # - Append '#' to the purl # - Strip the subpath from leading and trailing '/' # - Split this on '/' as segments # - Discard empty, '.' and '..' segments # - Percent-encode each segment # - UTF-8-encode each segment if needed in your programming language # - Join the segments with '/' # - Append this to the purl case @subpath in String => subpath unless subpath.empty? segments = [] subpath.delete_prefix('/').delete_suffix('/').split('/').each do |segment| next if segment.empty? || segment == '.' || segment == '..' segments << URI.encode_www_form_component(segment) end unless segments.empty? purl += '#' purl += segments.join('/') end else nil end purl end |