ecosystems-bibliothecary 14.3.0 → 15.0.1

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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +8 -23
  4. data/bibliothecary.gemspec +5 -9
  5. data/lib/bibliothecary/analyser.rb +0 -31
  6. data/lib/bibliothecary/cli.rb +35 -26
  7. data/lib/bibliothecary/configuration.rb +1 -6
  8. data/lib/bibliothecary/dependency.rb +1 -4
  9. data/lib/bibliothecary/parsers/bentoml.rb +0 -2
  10. data/lib/bibliothecary/parsers/bower.rb +0 -1
  11. data/lib/bibliothecary/parsers/cargo.rb +12 -10
  12. data/lib/bibliothecary/parsers/carthage.rb +51 -15
  13. data/lib/bibliothecary/parsers/clojars.rb +14 -18
  14. data/lib/bibliothecary/parsers/cocoapods.rb +100 -19
  15. data/lib/bibliothecary/parsers/cog.rb +0 -2
  16. data/lib/bibliothecary/parsers/conan.rb +156 -0
  17. data/lib/bibliothecary/parsers/conda.rb +0 -3
  18. data/lib/bibliothecary/parsers/cpan.rb +0 -2
  19. data/lib/bibliothecary/parsers/cran.rb +40 -19
  20. data/lib/bibliothecary/parsers/docker.rb +0 -2
  21. data/lib/bibliothecary/parsers/dub.rb +33 -8
  22. data/lib/bibliothecary/parsers/dvc.rb +0 -2
  23. data/lib/bibliothecary/parsers/elm.rb +13 -3
  24. data/lib/bibliothecary/parsers/go.rb +14 -5
  25. data/lib/bibliothecary/parsers/hackage.rb +132 -24
  26. data/lib/bibliothecary/parsers/haxelib.rb +14 -4
  27. data/lib/bibliothecary/parsers/hex.rb +37 -20
  28. data/lib/bibliothecary/parsers/homebrew.rb +0 -2
  29. data/lib/bibliothecary/parsers/julia.rb +0 -2
  30. data/lib/bibliothecary/parsers/maven.rb +35 -25
  31. data/lib/bibliothecary/parsers/meteor.rb +14 -4
  32. data/lib/bibliothecary/parsers/mlflow.rb +0 -2
  33. data/lib/bibliothecary/parsers/npm.rb +47 -59
  34. data/lib/bibliothecary/parsers/nuget.rb +22 -21
  35. data/lib/bibliothecary/parsers/ollama.rb +0 -2
  36. data/lib/bibliothecary/parsers/packagist.rb +0 -3
  37. data/lib/bibliothecary/parsers/pub.rb +0 -2
  38. data/lib/bibliothecary/parsers/pypi.rb +54 -35
  39. data/lib/bibliothecary/parsers/rubygems.rb +98 -27
  40. data/lib/bibliothecary/parsers/shard.rb +0 -1
  41. data/lib/bibliothecary/parsers/swift_pm.rb +77 -29
  42. data/lib/bibliothecary/parsers/vcpkg.rb +68 -17
  43. data/lib/bibliothecary/runner.rb +2 -15
  44. data/lib/bibliothecary/version.rb +1 -1
  45. data/lib/bibliothecary.rb +0 -4
  46. metadata +2 -110
  47. data/.codeclimate.yml +0 -25
  48. data/.github/CONTRIBUTING.md +0 -195
  49. data/.github/workflows/ci.yml +0 -25
  50. data/.gitignore +0 -10
  51. data/.rspec +0 -2
  52. data/.rubocop.yml +0 -69
  53. data/.ruby-version +0 -1
  54. data/.tidelift +0 -1
  55. data/CODE_OF_CONDUCT.md +0 -74
  56. data/Gemfile +0 -35
  57. data/Rakefile +0 -18
  58. data/bin/benchmark +0 -386
  59. data/bin/console +0 -15
  60. data/bin/setup +0 -8
  61. data/lib/bibliothecary/multi_parsers/bundler_like_manifest.rb +0 -26
  62. data/lib/bibliothecary/multi_parsers/cyclonedx.rb +0 -170
  63. data/lib/bibliothecary/multi_parsers/dependencies_csv.rb +0 -155
  64. data/lib/bibliothecary/multi_parsers/json_runtime.rb +0 -22
  65. data/lib/bibliothecary/multi_parsers/spdx.rb +0 -149
  66. data/lib/bibliothecary/purl_util.rb +0 -37
  67. data/lib/bibliothecary/runner/multi_manifest_filter.rb +0 -92
  68. data/lib/sdl_parser.rb +0 -30
@@ -1,11 +1,17 @@
1
1
  require "json"
2
- require "deb_control"
3
2
 
4
3
  module Bibliothecary
5
4
  module Parsers
6
5
  class Hackage
7
6
  include Bibliothecary::Analyser
8
7
 
8
+ # Matches dependency lines like: aeson == 1.1.* or base >= 4.9 && < 4.11
9
+ # Package names can contain letters, numbers, and hyphens
10
+ DEPENDENCY_REGEXP = /^\s*([a-zA-Z][a-zA-Z0-9-]*)\s*((?:[<>=!]+\s*[\d.*]+(?:\s*&&\s*[<>=!]+\s*[\d.*]+)*)?)/
11
+
12
+ # Matches build-tool-depends format: package:tool == version
13
+ BUILD_TOOL_REGEXP = /^\s*([a-zA-Z][a-zA-Z0-9-]*):[a-zA-Z][a-zA-Z0-9-]*\s*((?:[<>=!]+\s*[\d.*]+(?:\s*&&\s*[<>=!]+\s*[\d.*]+)*)?)/
14
+
9
15
  def self.mapping
10
16
  {
11
17
  match_extension(".cabal") => {
@@ -19,39 +25,140 @@ module Bibliothecary
19
25
  }
20
26
  end
21
27
 
22
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
23
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
24
- add_multi_parser(Bibliothecary::MultiParsers::Spdx)
25
28
 
26
29
  def self.parse_cabal(file_contents, options: {})
27
- source = options.fetch(:filename, 'package.cabal')
28
- headers = {
29
- "Content-Type" => "text/plain;charset=utf-8",
30
- }
30
+ source = options.fetch(:filename, "package.cabal")
31
+ deps = []
32
+
33
+ # Track current section type
34
+ current_section = nil
35
+ in_build_depends = false
36
+ in_build_tool_depends = false
37
+ current_deps_buffer = []
38
+
39
+ file_contents.each_line do |line|
40
+ # Check for section headers (library, executable, test-suite, benchmark)
41
+ if line =~ /^(library|executable|test-suite|benchmark)\b/i
42
+ current_section = $1.downcase
43
+ in_build_depends = false
44
+ in_build_tool_depends = false
45
+ next
46
+ end
47
+
48
+ # Check for build-depends: or build-tool-depends: (can be at any indentation level)
49
+ if line =~ /^\s*build-depends\s*:/i
50
+ in_build_depends = true
51
+ in_build_tool_depends = false
52
+ # Extract deps from same line after colon
53
+ deps_part = line.sub(/^\s*build-depends\s*:/i, "")
54
+ parse_deps_line(deps_part, deps, current_section, "build-depends", source)
55
+ next
56
+ end
57
+
58
+ if line =~ /^\s*build-tool-depends\s*:/i
59
+ in_build_tool_depends = true
60
+ in_build_depends = false
61
+ # Extract deps from same line after colon
62
+ deps_part = line.sub(/^\s*build-tool-depends\s*:/i, "")
63
+ parse_deps_line(deps_part, deps, current_section, "build-tool-depends", source)
64
+ next
65
+ end
66
+
67
+ # Check for other field headers that end depends section
68
+ # Field headers are like "field-name:" but NOT "package:tool" (build-tool-depends format)
69
+ # Build-tool-depends entries have format: package:tool version-constraint
70
+ if line =~ /^\s*([a-z][a-z0-9-]*)\s*:/i
71
+ field_name = $1
72
+ # If this looks like a field header (not package:tool), end the depends section
73
+ unless line =~ /^\s*[a-z][a-z0-9-]*:[a-z][a-z0-9-]*\s+/i
74
+ in_build_depends = false
75
+ in_build_tool_depends = false
76
+ next
77
+ end
78
+ end
79
+
80
+ # Continue parsing dependencies if in a depends section and line is indented
81
+ if (in_build_depends || in_build_tool_depends) && line =~ /^\s+/
82
+ dep_type = in_build_tool_depends ? "build-tool-depends" : "build-depends"
83
+ parse_deps_line(line, deps, current_section, dep_type, source)
84
+ elsif line !~ /^\s/
85
+ # Non-indented line that's not a section header ends depends
86
+ in_build_depends = false
87
+ in_build_tool_depends = false
88
+ end
89
+ end
90
+
91
+ ParserResult.new(dependencies: deps)
92
+ end
31
93
 
32
- response = Typhoeus.post("#{Bibliothecary.configuration.cabal_parser_host}/parse", headers: headers, body: file_contents, timeout: 60)
94
+ def self.parse_deps_line(line, deps, section, dep_type, source)
95
+ # Split by comma and parse each dep
96
+ line.split(",").each do |dep_str|
97
+ dep_str = dep_str.strip
98
+ next if dep_str.empty?
33
99
 
34
- raise Bibliothecary::RemoteParsingError.new("Http Error #{response.response_code} when contacting: #{Bibliothecary.configuration.cabal_parser_host}/parse", response.response_code) unless response.success?
35
- raw_deps = JSON.parse(response.body, symbolize_names: true)
36
- deps = raw_deps.map do |dep|
37
- Bibliothecary::Dependency.new(
100
+ # Use different regex for build-tool-depends (package:tool format)
101
+ regex = dep_type == "build-tool-depends" ? BUILD_TOOL_REGEXP : DEPENDENCY_REGEXP
102
+ match = dep_str.match(regex)
103
+ next unless match
104
+
105
+ name = match[1]
106
+ requirement = match[2]&.strip
107
+ requirement = "*" if requirement.nil? || requirement.empty?
108
+ # Normalize spacing: "== 1.1.*" -> "==1.1.*", ">= 4.9 && < 4.11" -> ">=4.9 && <4.11"
109
+ requirement = requirement.gsub(/([<>=!]+)\s+/, '\1').gsub(/\s+(&&)\s+/, ' \1 ')
110
+
111
+ # Determine type based on section and dep_type
112
+ type = determine_dep_type(section, dep_type)
113
+
114
+ deps << Dependency.new(
38
115
  platform: platform_name,
39
- name: dep[:name],
40
- requirement: dep[:requirement],
41
- type: dep[:type],
116
+ name: name,
117
+ requirement: requirement,
118
+ type: type,
42
119
  source: source
43
120
  )
44
121
  end
45
- Bibliothecary::ParserResult.new(dependencies: deps)
122
+ end
123
+
124
+ def self.determine_dep_type(section, dep_type)
125
+ if dep_type == "build-tool-depends"
126
+ "build"
127
+ elsif section == "test-suite"
128
+ "test"
129
+ elsif section == "benchmark"
130
+ "benchmark"
131
+ else
132
+ "runtime"
133
+ end
46
134
  end
47
135
 
48
136
  def self.parse_cabal_config(file_contents, options: {})
49
- source = options.fetch(:filename, 'cabal.config')
50
- manifest = DebControl::ControlFileBase.parse(file_contents)
51
- deps_raw = manifest.first["constraints"].delete("\n").split(",").map(&:strip)
52
- deps = deps_raw.map do |dependency|
53
- dep = dependency.delete("==").split(" ")
54
- Bibliothecary::Dependency.new(
137
+ source = options.fetch(:filename, "cabal.config")
138
+ deps = []
139
+
140
+ # Parse RFC822-style format: constraints: pkg1 ==1.0, pkg2 ==2.0, ...
141
+ # Values can span multiple lines (continuation lines start with whitespace)
142
+ constraints = nil
143
+ file_contents.each_line do |line|
144
+ if line =~ /^constraints:\s*(.*)/i
145
+ constraints = $1.strip
146
+ elsif line =~ /^\s+(.*)/ && constraints
147
+ constraints += " " + $1.strip
148
+ elsif line =~ /^[a-z]/i && constraints
149
+ break
150
+ end
151
+ end
152
+
153
+ return ParserResult.new(dependencies: []) unless constraints
154
+
155
+ constraints.split(",").each do |dep_str|
156
+ dep_str = dep_str.strip
157
+ next if dep_str.empty?
158
+
159
+ # Format: package ==version or package ==version.*
160
+ dep = dep_str.delete("==").split(/\s+/)
161
+ deps << Dependency.new(
55
162
  platform: platform_name,
56
163
  name: dep[0],
57
164
  requirement: dep[1] || "*",
@@ -59,7 +166,8 @@ module Bibliothecary
59
166
  source: source
60
167
  )
61
168
  end
62
- Bibliothecary::ParserResult.new(dependencies: deps)
169
+
170
+ ParserResult.new(dependencies: deps)
63
171
  end
64
172
  end
65
173
  end
@@ -6,19 +6,29 @@ module Bibliothecary
6
6
  module Parsers
7
7
  class Haxelib
8
8
  include Bibliothecary::Analyser
9
- extend Bibliothecary::MultiParsers::JSONRuntime
10
9
 
11
10
  def self.mapping
12
11
  {
13
12
  match_filename("haxelib.json") => {
14
13
  kind: "manifest",
15
- parser: :parse_json_runtime_manifest,
14
+ parser: :parse_manifest,
16
15
  },
17
16
  }
18
17
  end
19
18
 
20
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
21
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
19
+ def self.parse_manifest(file_contents, options: {})
20
+ manifest = JSON.parse(file_contents)
21
+ dependencies = manifest.fetch("dependencies", {}).map do |name, requirement|
22
+ Dependency.new(
23
+ name: name,
24
+ requirement: requirement,
25
+ type: "runtime",
26
+ source: options.fetch(:filename, nil),
27
+ platform: platform_name
28
+ )
29
+ end
30
+ ParserResult.new(dependencies: dependencies)
31
+ end
22
32
  end
23
33
  end
24
34
  end
@@ -5,6 +5,11 @@ module Bibliothecary
5
5
  class Hex
6
6
  include Bibliothecary::Analyser
7
7
 
8
+ # Matches mix.lock entries: "name": {:hex, :name, "version", ...
9
+ # or "name": {:git, "url", "ref", ...
10
+ HEX_LOCK_REGEXP = /"([^"]+)":\s*\{:hex,\s*:[^,]+,\s*"([^"]+)"/
11
+ GIT_LOCK_REGEXP = /"([^"]+)":\s*\{:git,\s*"([^"]+)",\s*"([^"]+)"/
12
+
8
13
  def self.mapping
9
14
  {
10
15
  match_filename("mix.exs") => {
@@ -18,44 +23,56 @@ module Bibliothecary
18
23
  }
19
24
  end
20
25
 
21
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
22
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
23
- add_multi_parser(Bibliothecary::MultiParsers::Spdx)
24
26
 
25
27
  def self.parse_mix(file_contents, options: {})
26
- source = options.fetch(:filename, 'mix.exs')
27
- response = Typhoeus.post("#{Bibliothecary.configuration.mix_parser_host}/", body: file_contents, timeout: 60)
28
- raise Bibliothecary::RemoteParsingError.new("Http Error #{response.response_code} when contacting: #{Bibliothecary.configuration.mix_parser_host}/", response.response_code) unless response.success?
29
- json = JSON.parse response.body
28
+ source = options.fetch(:filename, "mix.exs")
29
+ deps = []
30
+
31
+ # Remove comments before parsing
32
+ content = file_contents.gsub(/#.*$/, "")
30
33
 
31
- deps = json.map do |name, version|
32
- Bibliothecary::Dependency.new(
34
+ # Match deps in the dependencies list: {:name, "~> version"} or {:name, ">= version"}
35
+ # Format: {:dep_name, "requirement"} or {:dep_name, "requirement", opts}
36
+ content.scan(/\{:(\w+),\s*"([^"]+)"/) do |name, requirement|
37
+ deps << Dependency.new(
33
38
  platform: platform_name,
34
- name: name,
35
- requirement: version,
39
+ name: name.to_s,
40
+ requirement: requirement,
36
41
  type: "runtime",
37
42
  source: source
38
43
  )
39
44
  end
40
- Bibliothecary::ParserResult.new(dependencies: deps)
45
+
46
+ ParserResult.new(dependencies: deps)
41
47
  end
42
48
 
43
49
  def self.parse_mix_lock(file_contents, options: {})
44
- source = options.fetch(:filename, 'mix.lock')
45
- response = Typhoeus.post("#{Bibliothecary.configuration.mix_parser_host}/lock", body: file_contents, timeout: 60)
46
- raise Bibliothecary::RemoteParsingError.new("Http Error #{response.response_code} when contacting: #{Bibliothecary.configuration.mix_parser_host}/", response.response_code) unless response.success?
47
- json = JSON.parse response.body
50
+ source = options.fetch(:filename, "mix.lock")
51
+ deps = []
52
+
53
+ # Match hex packages: "name": {:hex, :name, "version", ...
54
+ file_contents.scan(HEX_LOCK_REGEXP) do |name, version|
55
+ deps << Dependency.new(
56
+ platform: platform_name,
57
+ name: name,
58
+ requirement: version,
59
+ type: "runtime",
60
+ source: source
61
+ )
62
+ end
48
63
 
49
- deps = json.map do |name, info|
50
- Bibliothecary::Dependency.new(
64
+ # Match git packages: "name": {:git, "url", "ref", ...
65
+ file_contents.scan(GIT_LOCK_REGEXP) do |name, _url, ref|
66
+ deps << Dependency.new(
51
67
  platform: platform_name,
52
68
  name: name,
53
- requirement: info["version"],
69
+ requirement: ref,
54
70
  type: "runtime",
55
71
  source: source
56
72
  )
57
73
  end
58
- Bibliothecary::ParserResult.new(dependencies: deps)
74
+
75
+ ParserResult.new(dependencies: deps)
59
76
  end
60
77
  end
61
78
  end
@@ -20,8 +20,6 @@ module Bibliothecary
20
20
  }
21
21
  end
22
22
 
23
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
24
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
25
23
 
26
24
  def self.parse_brewfile(file_contents, options: {})
27
25
  source = options.fetch(:filename, 'Brewfile')
@@ -14,8 +14,6 @@ module Bibliothecary
14
14
  }
15
15
  end
16
16
 
17
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
18
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
19
17
 
20
18
  def self.parse_require(file_contents, options: {})
21
19
  deps = []
@@ -133,9 +133,6 @@ module Bibliothecary
133
133
  }
134
134
  end
135
135
 
136
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
137
- add_multi_parser(Bibliothecary::MultiParsers::Spdx)
138
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
139
136
 
140
137
  def self.parse_ivy_manifest(file_contents, options: {})
141
138
  manifest = Ox.parse file_contents
@@ -195,15 +192,22 @@ module Bibliothecary
195
192
 
196
193
  def self.parse_gradle_resolved(file_contents, options: {})
197
194
  current_type = nil
195
+ source = options.fetch(:filename, nil)
196
+ dependencies = []
198
197
 
199
- dependencies = file_contents.split("\n").map do |line|
200
- current_type_match = GRADLE_TYPE_REGEXP.match(line)
201
- current_type = current_type_match.captures[0] if current_type_match
198
+ file_contents.each_line do |line|
199
+ line = line.chomp
202
200
 
203
- gradle_dep_match = GRADLE_DEP_REGEXP.match(line)
204
- next unless gradle_dep_match
201
+ # Check if this is a type header line (starts with word character)
202
+ # e.g. "annotationProcessor - Annotation processors..."
203
+ if line[0] =~ /\w/
204
+ current_type = line[GRADLE_TYPE_REGEXP, 1]
205
+ next
206
+ end
205
207
 
206
- split = gradle_dep_match.captures[0]
208
+ # Check for dependency line (contains +--- or \---)
209
+ split_match = line[GRADLE_DEP_REGEXP, 1]
210
+ next unless split_match
207
211
 
208
212
  # gradle can import on-disk projects and deps will be listed under them, e.g. `+--- project :test:integration`,
209
213
  # so we treat these projects as "internal" deps with requirement of "1.0.0"
@@ -217,7 +221,7 @@ module Bibliothecary
217
221
  end
218
222
 
219
223
  dep = line
220
- .split(split)[1]
224
+ .split(split_match)[1]
221
225
  .sub(GRADLE_LINE_ENDING_REGEXP, "")
222
226
  .sub(/ FAILED$/, "") # dependency could not be resolved (but still may have a version)
223
227
  .sub(" -> ", ":") # handle version arrow syntax
@@ -230,45 +234,50 @@ module Bibliothecary
230
234
 
231
235
  if dep.count == 6
232
236
  # get name from renamed package resolution "org:name:version -> renamed_org:name:version"
233
- Dependency.new(
237
+ dependencies << Dependency.new(
234
238
  original_name: dep[0, 2].join(":"),
235
239
  original_requirement: dep[2],
236
240
  name: dep[-3..-2].join(":"),
237
241
  requirement: dep[-1],
238
242
  type: current_type,
239
- source: options.fetch(:filename, nil),
243
+ source: source,
240
244
  platform: platform_name
241
245
  )
242
246
  elsif dep.count == 5
243
247
  # get name from renamed package resolution "org:name -> renamed_org:name:version"
244
- Dependency.new(
248
+ dependencies << Dependency.new(
245
249
  original_name: dep[0, 2].join(":"),
246
250
  original_requirement: "*",
247
251
  name: dep[-3..-2].join(":"),
248
252
  requirement: dep[-1],
249
253
  type: current_type,
250
- source: options.fetch(:filename, nil),
254
+ source: source,
251
255
  platform: platform_name
252
256
  )
253
257
  else
254
258
  # get name from version conflict resolution ("org:name:version -> version") and no-resolution ("org:name:version")
255
- Dependency.new(
259
+ dependencies << Dependency.new(
256
260
  name: dep[0..1].join(":"),
257
261
  requirement: dep[-1],
258
262
  type: current_type,
259
- source: options.fetch(:filename, nil),
263
+ source: source,
260
264
  platform: platform_name
261
265
  )
262
266
  end
263
267
  end
264
- .compact
265
- .uniq { |item| [item.name, item.requirement, item.type, item.original_name, item.original_requirement] }
268
+
269
+ dependencies.uniq! { |item| [item.name, item.requirement, item.type, item.original_name, item.original_requirement] }
266
270
  ParserResult.new(dependencies: dependencies)
267
271
  end
268
272
 
273
+ def self.strip_ansi(string)
274
+ return string unless string.include?("\033")
275
+
276
+ string.gsub(ANSI_MATCHER, "")
277
+ end
278
+
269
279
  def self.parse_maven_resolved(file_contents, options: {})
270
- dependencies = file_contents
271
- .gsub(ANSI_MATCHER, "")
280
+ dependencies = strip_ansi(file_contents)
272
281
  .split("\n")
273
282
  .map { |line| parse_resolved_dep_line(line, options: options) }
274
283
  .compact
@@ -281,11 +290,12 @@ module Bibliothecary
281
290
  # The depth-0 items are the (sub)project names
282
291
  # These are in the original order, with no de-duplication.
283
292
  def self.parse_maven_tree_items_with_depths(file_contents)
284
- file_contents
285
- .gsub(ANSI_MATCHER, "")
286
- .encode(universal_newline: true)
287
- # capture two groups; one is the ASCII art telling us the tree depth,
288
- # and two is the actual dependency
293
+ content = strip_ansi(file_contents)
294
+ content = content.gsub("\r\n", "\n").gsub("\r", "\n") if content.include?("\r")
295
+
296
+ # capture two groups; one is the ASCII art telling us the tree depth,
297
+ # and two is the actual dependency
298
+ content
289
299
  .scan(/^\[INFO\]\s((?:[-+|\\]|\s)*)((?:[\w.-]+:)+[\w.\-${}]+)/)
290
300
  # lines that start with "-" aren't part of the tree, example: "[INFO] --- dependency:3.8.1:tree"
291
301
  .reject { |(tree_ascii_art, _dep_info)| tree_ascii_art.start_with?("-") }
@@ -6,19 +6,29 @@ module Bibliothecary
6
6
  module Parsers
7
7
  class Meteor
8
8
  include Bibliothecary::Analyser
9
- extend Bibliothecary::MultiParsers::JSONRuntime
10
9
 
11
10
  def self.mapping
12
11
  {
13
12
  match_filename("versions.json") => {
14
13
  kind: "manifest",
15
- parser: :parse_json_runtime_manifest,
14
+ parser: :parse_manifest,
16
15
  },
17
16
  }
18
17
  end
19
18
 
20
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
21
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
19
+ def self.parse_manifest(file_contents, options: {})
20
+ manifest = JSON.parse(file_contents)
21
+ dependencies = manifest.fetch("dependencies", {}).map do |name, requirement|
22
+ Dependency.new(
23
+ name: name,
24
+ requirement: requirement,
25
+ type: "runtime",
26
+ source: options.fetch(:filename, nil),
27
+ platform: platform_name
28
+ )
29
+ end
30
+ ParserResult.new(dependencies: dependencies)
31
+ end
22
32
  end
23
33
  end
24
34
  end
@@ -15,8 +15,6 @@ module Bibliothecary
15
15
  }
16
16
  end
17
17
 
18
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
19
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
20
18
 
21
19
  def self.parse_mlmodel(file_contents, options: {})
22
20
  source = options.fetch(:filename, 'MLmodel')
@@ -43,9 +43,6 @@ module Bibliothecary
43
43
  }
44
44
  end
45
45
 
46
- add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
47
- add_multi_parser(Bibliothecary::MultiParsers::Spdx)
48
- add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
49
46
 
50
47
  def self.parse_package_lock(file_contents, options: {})
51
48
  manifest = JSON.parse(file_contents)
@@ -203,65 +200,56 @@ module Bibliothecary
203
200
  # version: "1.2.0",
204
201
  # }, ...]
205
202
  def self.parse_v1_yarn_lock(contents, source = nil)
206
- contents
207
- .encode(universal_newline: true)
208
- .gsub(/^#.*/, "")
209
- .strip
210
- .split("\n\n")
211
- .map do |chunk|
212
- requirements = chunk
213
- .lines
214
- .find { |l| !l.start_with?(" ") && l.strip.end_with?(":") } # first line, eg: '"@bar/foo@1.0.0", "@bar/foo@^1.0.1":'
215
- .strip
216
- .gsub(/"|:$/, "") # don't need quotes or trailing colon
217
- .split(",") # split the list of requirements
218
-
219
- name, alias_name = yarn_strip_npm_protocol(requirements.first) # if a package is aliased, strip the alias and return the real package name
220
- name = name.strip.split(/(?<!^)@/).first
221
- requirements = requirements.map { |d| d.strip.split(/(?<!^)@/, 2) } # split each requirement on name/version "@"", not on leading namespace "@"
222
- version = chunk.match(/version "?([^"]*)"?/)[1]
223
-
224
- {
225
- name: name,
226
- original_name: alias_name,
227
- requirements: requirements.map { |x| x[1] },
228
- original_requirement: alias_name.nil? ? nil : version,
229
- version: version,
230
- source: source,
231
- }
232
- end
203
+ deps = []
204
+ # Normalize line endings only if needed
205
+ contents = contents.gsub("\r\n", "\n").gsub("\r", "\n") if contents.include?("\r")
206
+
207
+ # Match package blocks: header line(s) ending with ":" followed by version line
208
+ # Header examples: 'package@version:' or '"package@version", "package@version2":'
209
+ contents.scan(/^([^\s#][^\n]*?):\s*\r?\n\s+version "?([^"\n]+)"?/m) do |header, version|
210
+ # Parse requirements from header (remove quotes and trailing colon)
211
+ requirements = header.gsub(/"/, "").split(",").map(&:strip)
212
+
213
+ name, alias_name = yarn_strip_npm_protocol(requirements.first)
214
+ name = name.strip.split(/(?<!^)@/).first
215
+ req_versions = requirements.map { |d| d.strip.split(/(?<!^)@/, 2) }
216
+
217
+ deps << {
218
+ name: name,
219
+ original_name: alias_name,
220
+ requirements: req_versions.map { |x| x[1] },
221
+ original_requirement: alias_name.nil? ? nil : version,
222
+ version: version,
223
+ source: source,
224
+ }
225
+ end
226
+ deps
233
227
  end
234
228
 
235
229
  def self.parse_v2_yarn_lock(contents, source = nil)
236
- parsed = YAML.load(contents)
237
- parsed = parsed.except("__metadata")
238
- parsed
239
- .reject do |packages, info|
240
- # yarn v4+ creates a lockfile entry: "myproject@workspace" with a "use.local" version
241
- # this lockfile entry is a reference to the project to which the lockfile belongs
242
- # skip this self-referential package
243
- (info["version"].to_s.include?("use.local") && packages.include?("workspace")) ||
244
- # yarn allows users to insert patches to their dependencies from within their project
245
- # these patches are marked as a separate entry in the lock file but do not represent a new dependency
246
- # and should be skipped here
247
- # https://yarnpkg.com/protocol/patch
248
- packages.include?("@patch:")
249
- end
250
- .map do |packages, info|
251
- packages = packages.split(", ")
252
- # use first requirement's name, assuming that deps will always resolve from deps of the same name
253
- name, alias_name = yarn_strip_npm_protocol(packages.first.rpartition("@").first)
254
- requirements = packages.map { |p| p.rpartition("@").last.gsub(/^.*:/, "") }
255
-
256
- {
257
- name: name,
258
- original_name: alias_name,
259
- requirements: requirements,
260
- original_requirement: alias_name.nil? ? nil : info["version"].to_s,
261
- version: info["version"].to_s,
262
- source: source,
263
- }
264
- end
230
+ deps = []
231
+ # Match package blocks: "package@npm:req": followed by version: x.y.z
232
+ # Examples: "js-tokens@npm:^3.0.0 || ^4.0.0": or "pkg1@npm:req1, pkg2@npm:req2":
233
+ contents.scan(/^"([^"]+)":\s*\n\s+version:\s*([^\n]+)/m) do |packages_str, version|
234
+ # Skip workspace/local packages and patches
235
+ next if version.include?("use.local") && packages_str.include?("workspace")
236
+ next if packages_str.include?("@patch:")
237
+
238
+ packages = packages_str.split(", ")
239
+ # use first requirement's name, assuming that deps will always resolve from deps of the same name
240
+ name, alias_name = yarn_strip_npm_protocol(packages.first.rpartition("@").first)
241
+ requirements = packages.map { |p| p.rpartition("@").last.gsub(/^.*:/, "") }
242
+
243
+ deps << {
244
+ name: name,
245
+ original_name: alias_name,
246
+ requirements: requirements,
247
+ original_requirement: alias_name.nil? ? nil : version.to_s,
248
+ version: version.to_s,
249
+ source: source,
250
+ }
251
+ end
252
+ deps
265
253
  end
266
254
 
267
255
  def self.parse_v5_pnpm_lock(parsed_contents, source = nil)