Class: Purl::PackageURL
- Inherits:
-
Object
- Object
- Purl::PackageURL
- Defined in:
- lib/purl/package_url.rb,
lib/purl/registry_url.rb
Overview
Add registry URL generation methods to PackageURL
Constant Summary collapse
- VALID_TYPE_CHARS =
/\A[a-zA-Z0-9\.\+\-]+\z/.freeze
- VALID_QUALIFIER_KEY_CHARS =
/\A[a-zA-Z0-9\.\-_]+\z/.freeze
Instance Attribute Summary collapse
-
#name ⇒ String
readonly
The package name.
-
#namespace ⇒ String?
readonly
The package namespace/scope.
-
#qualifiers ⇒ Hash<String, String>?
readonly
Key-value qualifier pairs.
-
#subpath ⇒ String?
readonly
Subpath within the package.
-
#type ⇒ String
readonly
The package type (e.g., “gem”, “npm”, “maven”).
-
#version ⇒ String?
readonly
The package version.
Class Method Summary collapse
-
.parse(purl_string) ⇒ PackageURL
Parse a PURL string into a PackageURL object.
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
Compare two PackageURL objects for equality.
-
#advisories(user_agent: nil, timeout: 10) ⇒ Array<Hash>
Look up security advisories using the advisories.ecosyste.ms API.
-
#deconstruct ⇒ Array
Pattern matching support for Ruby 2.7+.
-
#deconstruct_keys(keys) ⇒ Hash<Symbol, Object>
Pattern matching support for Ruby 2.7+ (hash patterns).
-
#hash ⇒ Integer
Generate hash code for the PackageURL.
-
#initialize(type:, name:, namespace: nil, version: nil, qualifiers: nil, subpath: nil) ⇒ PackageURL
constructor
Create a new PackageURL instance.
-
#lookup(user_agent: nil, timeout: 10) ⇒ Hash?
Look up package information using the ecosyste.ms API.
- #registry_url(base_url: nil) ⇒ Object
- #registry_url_with_version(base_url: nil) ⇒ Object
- #supports_registry_url? ⇒ Boolean
-
#to_h ⇒ Hash<Symbol, Object>
Convert the PackageURL to a hash representation.
-
#to_s ⇒ String
Convert the PackageURL to its canonical string representation.
-
#versionless ⇒ PackageURL
Create a new PackageURL without the version component.
-
#with(**changes) ⇒ PackageURL
Create a new PackageURL with modified attributes.
Constructor Details
#initialize(type:, name:, namespace: nil, version: nil, qualifiers: nil, subpath: nil) ⇒ PackageURL
Create a new PackageURL instance
74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/purl/package_url.rb', line 74 def initialize(type:, name:, namespace: nil, version: nil, qualifiers: nil, subpath: nil) @type = validate_and_normalize_type(type) @name = validate_name(name) @namespace = validate_namespace(namespace) @version = validate_version(version) if version @qualifiers = validate_qualifiers(qualifiers) if qualifiers @subpath = validate_subpath(subpath) if subpath # Apply post-validation normalization that depends on other components apply_post_validation_normalization end |
Instance Attribute Details
#name ⇒ String (readonly)
Returns the package name.
40 41 42 |
# File 'lib/purl/package_url.rb', line 40 def name @name end |
#namespace ⇒ String? (readonly)
Returns the package namespace/scope.
37 38 39 |
# File 'lib/purl/package_url.rb', line 37 def namespace @namespace end |
#qualifiers ⇒ Hash<String, String>? (readonly)
Returns key-value qualifier pairs.
46 47 48 |
# File 'lib/purl/package_url.rb', line 46 def qualifiers @qualifiers end |
#subpath ⇒ String? (readonly)
Returns subpath within the package.
49 50 51 |
# File 'lib/purl/package_url.rb', line 49 def subpath @subpath end |
#type ⇒ String (readonly)
Returns the package type (e.g., “gem”, “npm”, “maven”).
34 35 36 |
# File 'lib/purl/package_url.rb', line 34 def type @type end |
#version ⇒ String? (readonly)
Returns the package version.
43 44 45 |
# File 'lib/purl/package_url.rb', line 43 def version @version end |
Class Method Details
.parse(purl_string) ⇒ PackageURL
Parse a PURL string into a PackageURL object
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 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/purl/package_url.rb', line 105 def self.parse(purl_string) raise InvalidSchemeError, "PURL must start with 'pkg:'" unless purl_string.start_with?("pkg:") # Remove the pkg: prefix and any leading slashes (they're not significant) remainder = purl_string[4..-1] remainder = remainder.sub(/\A\/+/, "") if remainder.start_with?("/") # Split off qualifiers (query string) first if remainder.include?("?") path_and_version, query_string = remainder.split("?", 2) else path_and_version = remainder query_string = nil end # Parse version and subpath according to PURL spec # Format: pkg:type/namespace/name@version#subpath version = nil subpath = nil # First split on # to separate subpath if path_and_version.include?("#") path_and_version_part, subpath_part = path_and_version.split("#", 2) # Clean up subpath - remove leading/trailing slashes and decode components if subpath_part && !subpath_part.empty? subpath_clean = subpath_part.strip subpath_clean = subpath_clean[1..-1] if subpath_clean.start_with?("/") subpath_clean = subpath_clean[0..-2] if subpath_clean.end_with?("/") unless subpath_clean.empty? # Decode each component separately to handle paths properly subpath_components = subpath_clean.split("/").map { |part| URI.decode_www_form_component(part) } subpath = subpath_components.join("/") end end else path_and_version_part = path_and_version end # Then split on @ to separate version if path_and_version_part.include?("@") # Find the last @ to handle cases like @babel/[email protected] at_index = path_and_version_part.rindex("@") path_part = path_and_version_part[0...at_index] version_part = path_and_version_part[at_index + 1..-1] version = URI.decode_www_form_component(version_part) unless version_part.empty? else path_part = path_and_version_part end # Check if path ends with slash (indicates empty name component) empty_name_component = path_part.end_with?("/") path_part = path_part.chomp("/") if empty_name_component # Parse the path components path_components = path_part.split("/") raise MalformedUrlError, "PURL path cannot be empty" if path_components.empty? || path_components == [""] # First component is always the type type = URI.decode_www_form_component(path_components.shift) raise MalformedUrlError, "PURL must have a name component" if path_components.empty? # Handle empty name component (trailing slash case) if empty_name_component # All remaining components become namespace, name is nil if path_components.length == 1 # Just type/ - invalid, should have been caught earlier name = nil namespace = nil else # All non-type components become namespace name = nil if path_components.length == 1 namespace = URI.decode_www_form_component(path_components[0]) else namespace = path_components.map { |part| URI.decode_www_form_component(part) }.join("/") end end else # Normal parsing logic # For simple cases like gem/rails, there's just the name # For namespaced cases like npm/@babel/core, @babel is namespace, core is name if path_components.length == 1 # Simple case: just type/name name = URI.decode_www_form_component(path_components[0]) namespace = nil else # Multiple components - assume last is name, others are namespace name = URI.decode_www_form_component(path_components.pop) # Everything else is namespace if path_components.length == 1 namespace = URI.decode_www_form_component(path_components[0]) else # Multiple remaining components - treat as namespace joined together namespace = path_components.map { |part| URI.decode_www_form_component(part) }.join("/") end end end # Parse qualifiers from query string qualifiers = parse_qualifiers(query_string) if query_string new( type: type, name: name, namespace: namespace, version: version, qualifiers: qualifiers, subpath: subpath ) end |
Instance Method Details
#==(other) ⇒ Boolean
Compare two PackageURL objects for equality
Two PURLs are equal if their canonical string representations are identical.
305 306 307 308 309 |
# File 'lib/purl/package_url.rb', line 305 def ==(other) return false unless other.is_a?(PackageURL) to_s == other.to_s end |
#advisories(user_agent: nil, timeout: 10) ⇒ Array<Hash>
Look up security advisories using the advisories.ecosyste.ms API
404 405 406 407 408 |
# File 'lib/purl/package_url.rb', line 404 def advisories(user_agent: nil, timeout: 10) require_relative "advisory" advisory_client = Advisory.new(user_agent: user_agent, timeout: timeout) advisory_client.lookup(self) end |
#deconstruct ⇒ Array
Pattern matching support for Ruby 2.7+
Allows destructuring PackageURL in pattern matching.
329 330 331 |
# File 'lib/purl/package_url.rb', line 329 def deconstruct [type, namespace, name, version, qualifiers, subpath] end |
#deconstruct_keys(keys) ⇒ Hash<Symbol, Object>
Pattern matching support for Ruby 2.7+ (hash patterns)
343 344 345 346 |
# File 'lib/purl/package_url.rb', line 343 def deconstruct_keys(keys) return to_h.slice(*keys) if keys to_h end |
#hash ⇒ Integer
Generate hash code for the PackageURL
314 315 316 |
# File 'lib/purl/package_url.rb', line 314 def hash to_s.hash end |
#lookup(user_agent: nil, timeout: 10) ⇒ Hash?
Look up package information using the ecosyste.ms API
387 388 389 390 391 |
# File 'lib/purl/package_url.rb', line 387 def lookup(user_agent: nil, timeout: 10) require_relative "lookup" lookup_client = Lookup.new(user_agent: user_agent, timeout: timeout) lookup_client.package_info(self) end |
#registry_url(base_url: nil) ⇒ Object
638 639 640 |
# File 'lib/purl/registry_url.rb', line 638 def registry_url(base_url: nil) RegistryURL.generate(self, base_url: base_url) end |
#registry_url_with_version(base_url: nil) ⇒ Object
642 643 644 |
# File 'lib/purl/registry_url.rb', line 642 def registry_url_with_version(base_url: nil) RegistryURL.new(self).generate_with_version(base_url: base_url) end |
#supports_registry_url? ⇒ Boolean
646 647 648 |
# File 'lib/purl/registry_url.rb', line 646 def supports_registry_url? RegistryURL.supports?(type) end |
#to_h ⇒ Hash<Symbol, Object>
Convert the PackageURL to a hash representation
283 284 285 286 287 288 289 290 291 292 |
# File 'lib/purl/package_url.rb', line 283 def to_h { type: type, namespace: namespace, name: name, version: version, qualifiers: qualifiers, subpath: subpath } end |
#to_s ⇒ String
Convert the PackageURL to its canonical string representation
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 |
# File 'lib/purl/package_url.rb', line 225 def to_s parts = ["pkg:", type.downcase] if namespace # Encode namespace parts, but preserve the structure namespace_parts = namespace.split("/").map do |part| URI.encode_www_form_component(part) end parts << "/" << namespace_parts.join("/") end parts << "/" << URI.encode_www_form_component(name) if version # Special handling for version encoding - don't encode colon in certain contexts encoded_version = case type&.downcase when "docker" # Docker versions with sha256: should not encode the colon version.gsub("sha256:", "sha256:") else URI.encode_www_form_component(version) end parts << "@" << encoded_version end if subpath # Subpath goes after # according to PURL spec # Normalize the subpath to remove . and .. components normalized_subpath = self.class.normalize_subpath(subpath) if normalized_subpath subpath_parts = normalized_subpath.split("/").map { |part| URI.encode_www_form_component(part) } parts << "#" << subpath_parts.join("/") end end if qualifiers && !qualifiers.empty? query_parts = qualifiers.sort.map do |key, value| # Keys are already normalized to lowercase during parsing/validation # Values should not be encoded for certain safe characters in PURL spec encoded_key = key # Key is already clean encoded_value = value.to_s # Don't encode values to match canonical form "#{encoded_key}=#{encoded_value}" end parts << "?" << query_parts.join("&") end parts.join end |
#versionless ⇒ PackageURL
Create a new PackageURL without the version component
371 372 373 |
# File 'lib/purl/package_url.rb', line 371 def versionless with(version: nil) end |
#with(**changes) ⇒ PackageURL
Create a new PackageURL with modified attributes
357 358 359 360 361 |
# File 'lib/purl/package_url.rb', line 357 def with(**changes) current_attrs = to_h new_attrs = current_attrs.merge(changes) self.class.new(**new_attrs) end |