bibliothecary 8.6.4 → 8.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89c5d8cb9fca421230fa967e5fbc3ce68d9178b0c065cb0c20737464739fab4e
4
- data.tar.gz: 7ae9d50b5a82ae46f2c473eee0b483f2376674c97dd94ff8b1c70690973dd3d5
3
+ metadata.gz: 39af4653f19e0376f945656791b9dc2eb6e60f5d519a9a0e947e4623827376d2
4
+ data.tar.gz: c2a6b01a1d14abffde071d07a95a28e851e85c61a89fe3ebb5239c82f2cef3fd
5
5
  SHA512:
6
- metadata.gz: 29b2d8750cb9611c3a7cef9946fbaf12e8eb6d73d95ecbd604b4ac41c4428a07361f8d67db8ccece341d1f5c59c2b70510695f947bc060ae7a44f3e395126589
7
- data.tar.gz: 418b9879a3028dc396e996972c49be58fb1a63a3ec707e035d7b9c9cccdbdcefe919ff741a37d71a719cffc15145b4f362534ed83e72e8debbff2e73c4ffdee8
6
+ metadata.gz: 5e923a36a18760a6f3acdc05f7330de7704c42f235fd1bbee880dc7d7ea3bf1604ea8ecd060f6984db7a32ac6b552d10cd19d8b6257153fc7c19f55430234661
7
+ data.tar.gz: a65aed02e5de4338a4c48ce44f77b2119fee0a26be51c7bac1bd18cdf8a51ceebed0305e16b965483cf4d2452ba63f3557de93acd29b474428245d62bceac707
@@ -17,26 +17,6 @@ module Bibliothecary
17
17
  NoComponents = Class.new(StandardError)
18
18
 
19
19
  class ManifestEntries
20
- # If a purl type (key) exists, it will be used in a manifest for
21
- # the key's value. If not, it's ignored.
22
- #
23
- # https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst
24
- PURL_TYPE_MAPPING = {
25
- "golang" => :go,
26
- "maven" => :maven,
27
- "npm" => :npm,
28
- "cargo" => :cargo,
29
- "composer" => :packagist,
30
- "conda" => :conda,
31
- "cran" => :cran,
32
- "gem" => :rubygems,
33
- "hackage" => :hackage,
34
- "hex" => :hex,
35
- "nuget" => :nuget,
36
- "pypi" => :pypi,
37
- "swift" => :swift_pm
38
- }
39
-
40
20
  attr_reader :manifests
41
21
 
42
22
  def initialize(parse_queue:)
@@ -49,7 +29,7 @@ module Bibliothecary
49
29
  end
50
30
 
51
31
  def <<(purl)
52
- mapping = PURL_TYPE_MAPPING[purl.type]
32
+ mapping = Bibliothecary::PURL_TYPE_MAPPING[purl.type]
53
33
  return unless mapping
54
34
 
55
35
  @manifests[mapping] ||= Set.new
@@ -0,0 +1,98 @@
1
+ # packageurl-ruby uses pattern-matching (https://docs.ruby-lang.org/en/2.7.0/NEWS.html#label-Pattern+matching)
2
+ # which warns a whole bunch in Ruby 2.7 as being an experimental feature, but has
3
+ # been accepted in Ruby 3.0 (https://rubyreferences.github.io/rubychanges/3.0.html#pattern-matching).
4
+ Warning[:experimental] = false
5
+ require 'package_url'
6
+ Warning[:experimental] = true
7
+
8
+ module Bibliothecary
9
+ module MultiParsers
10
+ module Spdx
11
+ include Bibliothecary::Analyser
12
+ include Bibliothecary::Analyser::TryCache
13
+
14
+ # e.g. 'SomeText:' (allowing for leading whitespace)
15
+ WELLFORMED_LINE_REGEX = /^\s*[a-zA-Z]+:/
16
+
17
+ # e.g. 'PackageName: (allowing for excessive whitespace)
18
+ PACKAGE_NAME_REGEX = /^\s*PackageName:\s*(.*)/
19
+
20
+ # e.g. 'PackageVersion:' (allowing for excessive whitespace)
21
+ PACKAGE_VERSION_REGEX =/^\s*PackageVersion:\s*(.*)/
22
+
23
+ # e.g. "ExternalRef: PACKAGE-MANAGER purl (allowing for excessive whitespace)
24
+ PURL_REGEX = /^\s*ExternalRef:\s*PACKAGE[-|_]MANAGER\s*purl\s*(.*)/
25
+
26
+ NoEntries = Class.new(StandardError)
27
+ MalformedFile = Class.new(StandardError)
28
+
29
+ def self.mapping
30
+ {
31
+ match_extension('.spdx') => {
32
+ kind: 'lockfile',
33
+ parser: :parse_spdx_tag_value,
34
+ ungroupable: true
35
+ }
36
+ }
37
+ end
38
+
39
+ def parse_spdx_tag_value(file_contents, options: {})
40
+ entries = try_cache(options, options[:filename]) do
41
+ parse_spdx_tag_value_file_contents(file_contents)
42
+ end
43
+
44
+ raise NoEntries if entries.empty?
45
+
46
+ entries[platform_name.to_sym]
47
+ end
48
+
49
+ def get_platform(purl_string)
50
+ platform = PackageURL.parse(purl_string).type
51
+
52
+ Bibliothecary::PURL_TYPE_MAPPING[platform]
53
+ end
54
+
55
+ def parse_spdx_tag_value_file_contents(file_contents)
56
+ entries = {}
57
+
58
+ package_name = nil
59
+ package_version = nil
60
+ platform = nil
61
+
62
+ file_contents.split("\n").each do |line|
63
+ stripped_line = line.strip
64
+
65
+ next if skip_line?(stripped_line)
66
+
67
+ raise MalformedFile unless stripped_line.match(WELLFORMED_LINE_REGEX)
68
+
69
+ if (match = stripped_line.match(PACKAGE_NAME_REGEX))
70
+ package_name = match[1]
71
+ elsif (match = stripped_line.match(PACKAGE_VERSION_REGEX))
72
+ package_version = match[1]
73
+ elsif (match = stripped_line.match(PURL_REGEX))
74
+ platform ||= get_platform(match[1])
75
+ end
76
+
77
+ unless package_name.nil? || package_version.nil? || platform.nil?
78
+ entries[platform.to_sym] ||= []
79
+ entries[platform.to_sym] << {
80
+ name: package_name,
81
+ requirement: package_version,
82
+ type: 'lockfile'
83
+ }
84
+
85
+ package_name = package_version = platform = nil
86
+ end
87
+ end
88
+
89
+ entries
90
+ end
91
+
92
+ def skip_line?(stripped_line)
93
+ # Ignore blank lines and comments
94
+ stripped_line == "" || stripped_line[0] == "#"
95
+ end
96
+ end
97
+ end
98
+ end
@@ -17,6 +17,7 @@ module Bibliothecary
17
17
  end
18
18
 
19
19
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
20
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
20
21
  add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
21
22
 
22
23
  def self.parse_manifest(file_contents, options: {})
@@ -28,6 +28,7 @@ module Bibliothecary
28
28
 
29
29
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
30
30
  add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
31
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
31
32
 
32
33
  def self.parse_conda(file_contents, options: {})
33
34
  parse_conda_with_kind(file_contents, "manifest")
@@ -18,6 +18,7 @@ module Bibliothecary
18
18
 
19
19
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
20
20
  add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
21
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
21
22
 
22
23
  def self.parse_description(file_contents, options: {})
23
24
  manifest = DebControl::ControlFileBase.parse(file_contents)
@@ -66,6 +66,7 @@ module Bibliothecary
66
66
  end
67
67
 
68
68
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
69
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
69
70
  add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
70
71
 
71
72
  def self.parse_godep_json(file_contents, options: {})
@@ -21,6 +21,7 @@ module Bibliothecary
21
21
 
22
22
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
23
23
  add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
24
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
24
25
 
25
26
  def self.parse_cabal(file_contents, options: {})
26
27
  headers = {
@@ -20,6 +20,7 @@ module Bibliothecary
20
20
 
21
21
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
22
22
  add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
23
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
23
24
 
24
25
  def self.parse_mix(file_contents, options: {})
25
26
  response = Typhoeus.post("#{Bibliothecary.configuration.mix_parser_host}/", body: file_contents)
@@ -1,6 +1,10 @@
1
1
  require 'ox'
2
2
  require 'strings-ansi'
3
3
 
4
+ # Known shortcomings and unimplemented Maven features:
5
+ # pom.xml
6
+ # <exclusions> cannot be taken into account (because it requires knowledge of transitive deps)
7
+ # <properties> are the only thing inherited from parent poms currenly
4
8
  module Bibliothecary
5
9
  module Parsers
6
10
  class Maven
@@ -25,15 +29,15 @@ module Bibliothecary
25
29
  # Deprecated methods: https://docs.gradle.org/current/userguide/upgrading_version_6.html#sec:configuration_removal
26
30
  GRADLE_DEPENDENCY_METHODS = %w(api compile compileClasspath compileOnly compileOnlyApi implementation runtime runtimeClasspath runtimeOnly testCompile testCompileOnly testImplementation testRuntime testRuntimeOnly)
27
31
 
28
- # Intentionally overly-simplified regexes to scrape deps from build.gradle (Groovy) and build.gradle.kts (Kotlin) files.
29
- # To be truly useful bibliothecary would need full Groovy / Kotlin parsers that speaks Gradle,
32
+ # Intentionally overly-simplified regexes to scrape deps from build.gradle (Groovy) and build.gradle.kts (Kotlin) files.
33
+ # To be truly useful bibliothecary would need full Groovy / Kotlin parsers that speaks Gradle,
30
34
  # because the Groovy and Kotlin DSLs have many dynamic ways of declaring dependencies.
31
35
  GRADLE_VERSION_REGEX = /[\w.-]+/ # e.g. '1.2.3'
32
36
  GRADLE_VAR_INTERPOLATION_REGEX = /\$\w+/ # e.g. '$myVersion'
33
37
  GRADLE_CODE_INTERPOLATION_REGEX = /\$\{.*\}/ # e.g. '${my-project-settings["version"]}'
34
38
  GRADLE_GAV_REGEX = /([\w.-]+)\:([\w.-]+)(?:\:(#{GRADLE_VERSION_REGEX}|#{GRADLE_VAR_INTERPOLATION_REGEX}|#{GRADLE_CODE_INTERPOLATION_REGEX}))?/ # e.g. "group:artifactId:1.2.3"
35
- GRADLE_GROOVY_SIMPLE_REGEX = /(#{GRADLE_DEPENDENCY_METHODS.join('|')})\s*\(?\s*['"]#{GRADLE_GAV_REGEX}['"]/m
36
- GRADLE_KOTLIN_SIMPLE_REGEX = /(#{GRADLE_DEPENDENCY_METHODS.join('|')})\s*\(\s*"#{GRADLE_GAV_REGEX}"/m
39
+ GRADLE_GROOVY_SIMPLE_REGEX = /(#{GRADLE_DEPENDENCY_METHODS.join('|')})\s*\(?\s*['"]#{GRADLE_GAV_REGEX}['"]/m
40
+ GRADLE_KOTLIN_SIMPLE_REGEX = /(#{GRADLE_DEPENDENCY_METHODS.join('|')})\s*\(\s*"#{GRADLE_GAV_REGEX}"/m
37
41
 
38
42
  MAVEN_PROPERTY_REGEX = /\$\{(.+?)\}/
39
43
  MAX_DEPTH = 5
@@ -96,6 +100,7 @@ module Bibliothecary
96
100
  end
97
101
 
98
102
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
103
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
99
104
  add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
100
105
 
101
106
  def self.parse_ivy_manifest(file_contents, options: {})
@@ -162,7 +167,7 @@ module Bibliothecary
162
167
  # so we treat these projects as "internal" deps with requirement of "1.0.0"
163
168
  if (project_match = line.match(GRADLE_PROJECT_REGEX))
164
169
  # an empty project name is self-referential (i.e. a cycle), and we don't need to track the manifest's project itself, e.g. "+--- project :"
165
- next if project_match[1].nil?
170
+ next if project_match[1].nil?
166
171
 
167
172
  # project names can have colons (e.g. for gradle projects in subfolders), which breaks maven artifact naming assumptions, so just replace them with hyphens.
168
173
  project_name = project_match[1].gsub(/:/, "-")
@@ -269,18 +274,40 @@ module Bibliothecary
269
274
  manifest = Ox.parse file_contents
270
275
  xml = manifest.respond_to?('project') ? manifest.project : manifest
271
276
  [].tap do |deps|
272
- ['dependencies/dependency', 'dependencyManagement/dependencies/dependency'].each do |deps_xpath|
273
- xml.locate(deps_xpath).each do |dep|
274
- dep_hash = {
275
- name: "#{extract_pom_dep_info(xml, dep, 'groupId', parent_properties)}:#{extract_pom_dep_info(xml, dep, 'artifactId', parent_properties)}",
276
- requirement: extract_pom_dep_info(xml, dep, 'version', parent_properties),
277
- type: extract_pom_dep_info(xml, dep, 'scope', parent_properties) || 'runtime',
278
- }
279
- # optional field is, itself, optional, and will be either "true" or "false"
280
- optional = extract_pom_dep_info(xml, dep, 'optional', parent_properties)
281
- dep_hash[:optional] = optional == "true" unless optional.nil?
282
- deps.push(dep_hash)
277
+ # <dependencyManagement> is a namespace to specify artifact configuration (e.g. version), but it doesn't
278
+ # actually add dependencies to your project. Grab these and keep them for reference while parsing <dependencies>
279
+ # Ref: https://maven.apache.org/pom.html#Dependency_Management
280
+ # Ref: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#transitive-dependencies
281
+ dependencyManagement = xml.locate("dependencyManagement/dependencies/dependency").map do |dep|
282
+ {
283
+ groupId: extract_pom_dep_info(xml, dep, "groupId", parent_properties),
284
+ artifactId: extract_pom_dep_info(xml, dep, "artifactId", parent_properties),
285
+ version: extract_pom_dep_info(xml, dep, "version", parent_properties),
286
+ scope: extract_pom_dep_info(xml, dep, "scope", parent_properties),
287
+ }
288
+ end
289
+ # <dependencies> is the namespace that will add dependencies to your project.
290
+ xml.locate("dependencies/dependency").each do |dep|
291
+ groupId = extract_pom_dep_info(xml, dep, 'groupId', parent_properties)
292
+ artifactId = extract_pom_dep_info(xml, dep, 'artifactId', parent_properties)
293
+ version = extract_pom_dep_info(xml, dep, 'version', parent_properties)
294
+ scope = extract_pom_dep_info(xml, dep, 'scope', parent_properties)
295
+
296
+ # Use any dep configurations from <dependencyManagement> as fallbacks
297
+ if (depConfig = dependencyManagement.find { |d| d[:groupId] == groupId && d[:artifactId] == artifactId })
298
+ version ||= depConfig[:version]
299
+ scope ||= depConfig[:scope]
283
300
  end
301
+
302
+ dep_hash = {
303
+ name: "#{groupId}:#{artifactId}",
304
+ requirement: version,
305
+ type: scope || 'runtime',
306
+ }
307
+ # optional field is, itself, optional, and will be either "true" or "false"
308
+ optional = extract_pom_dep_info(xml, dep, 'optional', parent_properties)
309
+ dep_hash[:optional] = optional == "true" unless optional.nil?
310
+ deps.push(dep_hash)
284
311
  end
285
312
  end
286
313
  end
@@ -34,6 +34,7 @@ module Bibliothecary
34
34
  end
35
35
 
36
36
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
37
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
37
38
  add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
38
39
 
39
40
  def self.parse_package_lock(file_contents, options: {})
@@ -57,9 +58,9 @@ module Bibliothecary
57
58
  def self.parse_package_lock_v1(manifest)
58
59
  parse_package_lock_deps_recursively(manifest.fetch('dependencies', []))
59
60
  end
60
-
61
+
61
62
  def self.parse_package_lock_v2(manifest)
62
- # "packages" is a flat object where each key is the installed location of the dep, e.g. node_modules/foo/node_modules/bar.
63
+ # "packages" is a flat object where each key is the installed location of the dep, e.g. node_modules/foo/node_modules/bar.
63
64
  manifest
64
65
  .fetch("packages")
65
66
  .reject { |name, dep| name == "" } # this is the lockfile's package itself
@@ -68,7 +69,7 @@ module Bibliothecary
68
69
  name: name.split("node_modules/").last,
69
70
  requirement: dep["version"],
70
71
  type: dep.fetch("dev", false) || dep.fetch("devOptional", false) ? "development" : "runtime"
71
- }
72
+ }
72
73
  end
73
74
  end
74
75
 
@@ -94,7 +95,7 @@ module Bibliothecary
94
95
  def self.parse_manifest(file_contents, options: {})
95
96
  manifest = JSON.parse(file_contents)
96
97
  raise "appears to be a lockfile rather than manifest format" if manifest.key?('lockfileVersion')
97
-
98
+
98
99
  (
99
100
  map_dependencies(manifest, 'dependencies', 'runtime') +
100
101
  map_dependencies(manifest, 'devDependencies', 'development')
@@ -46,6 +46,7 @@ module Bibliothecary
46
46
 
47
47
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
48
48
  add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
49
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
49
50
 
50
51
  def self.parse_project_lock_json(file_contents, options: {})
51
52
  manifest = JSON.parse file_contents
@@ -20,6 +20,7 @@ module Bibliothecary
20
20
 
21
21
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
22
22
  add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
23
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
23
24
 
24
25
  def self.parse_lockfile(file_contents, options: {})
25
26
  manifest = JSON.parse file_contents
@@ -87,6 +87,7 @@ module Bibliothecary
87
87
 
88
88
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
89
89
  add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
90
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
90
91
 
91
92
  def self.parse_pipfile(file_contents, options: {})
92
93
  manifest = Tomlrb.parse(file_contents)
@@ -94,10 +95,10 @@ module Bibliothecary
94
95
  end
95
96
 
96
97
  def self.parse_pyproject(file_contents, options: {})
97
- deps = []
98
+ deps = []
98
99
 
99
100
  file_contents = Tomlrb.parse(file_contents)
100
-
101
+
101
102
  # Parse poetry [tool.poetry] deps
102
103
  poetry_manifest = file_contents.fetch('tool', {}).fetch('poetry', {})
103
104
  deps += map_dependencies(poetry_manifest['dependencies'], 'runtime')
@@ -32,6 +32,7 @@ module Bibliothecary
32
32
 
33
33
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
34
34
  add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
35
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
35
36
 
36
37
  def self.parse_gemfile_lock(file_contents, options: {})
37
38
  file_contents.lines(chomp: true).map do |line|
@@ -14,6 +14,7 @@ module Bibliothecary
14
14
 
15
15
  add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
16
16
  add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
17
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
17
18
 
18
19
  def self.parse_package_swift(file_contents, options: {})
19
20
  response = Typhoeus.post("#{Bibliothecary.configuration.swift_parser_host}/to-json", body: file_contents)
@@ -0,0 +1,21 @@
1
+ module Bibliothecary
2
+ # If a purl type (key) exists, it will be used in a manifest for
3
+ # the key's value. If not, it's ignored.
4
+ #
5
+ # https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst
6
+ PURL_TYPE_MAPPING = {
7
+ "golang" => :go,
8
+ "maven" => :maven,
9
+ "npm" => :npm,
10
+ "cargo" => :cargo,
11
+ "composer" => :packagist,
12
+ "conda" => :conda,
13
+ "cran" => :cran,
14
+ "gem" => :rubygems,
15
+ "hackage" => :hackage,
16
+ "hex" => :hex,
17
+ "nuget" => :nuget,
18
+ "pypi" => :pypi,
19
+ "swift" => :swift_pm
20
+ }.freeze
21
+ end
@@ -1,3 +1,3 @@
1
1
  module Bibliothecary
2
- VERSION = "8.6.4"
2
+ VERSION = "8.7.0"
3
3
  end
data/lib/bibliothecary.rb CHANGED
@@ -5,6 +5,7 @@ require "bibliothecary/runner"
5
5
  require "bibliothecary/exceptions"
6
6
  require "bibliothecary/file_info"
7
7
  require "bibliothecary/related_files_info"
8
+ require "bibliothecary/purl_util"
8
9
  require "find"
9
10
  require "tomlrb"
10
11
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bibliothecary
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.6.4
4
+ version: 8.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-02 00:00:00.000000000 Z
11
+ date: 2023-09-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tomlrb
@@ -290,6 +290,7 @@ files:
290
290
  - lib/bibliothecary/multi_parsers/cyclonedx.rb
291
291
  - lib/bibliothecary/multi_parsers/dependencies_csv.rb
292
292
  - lib/bibliothecary/multi_parsers/json_runtime.rb
293
+ - lib/bibliothecary/multi_parsers/spdx.rb
293
294
  - lib/bibliothecary/parsers/bower.rb
294
295
  - lib/bibliothecary/parsers/cargo.rb
295
296
  - lib/bibliothecary/parsers/carthage.rb
@@ -315,6 +316,7 @@ files:
315
316
  - lib/bibliothecary/parsers/rubygems.rb
316
317
  - lib/bibliothecary/parsers/shard.rb
317
318
  - lib/bibliothecary/parsers/swift_pm.rb
319
+ - lib/bibliothecary/purl_util.rb
318
320
  - lib/bibliothecary/related_files_info.rb
319
321
  - lib/bibliothecary/runner.rb
320
322
  - lib/bibliothecary/runner/multi_manifest_filter.rb