bibliothecary 14.1.0 → 14.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8220f939e62ba68f9f9edebda8c1d5901d88f028c9caea6b173c2f9da14cb5b4
4
- data.tar.gz: 2920b4cf85f4497bf1a2749e8c6c6b2cdfa5c0c0a9f3cb244bc86c7991041974
3
+ metadata.gz: 6707fc0443320dc26cce55e6adcd048042c13b1648efe3a134952c62e5288f1f
4
+ data.tar.gz: 7e53ee490a0f3eb7ab97470d30332247176c8ea73fdf88182c82da152de50c56
5
5
  SHA512:
6
- metadata.gz: 768d17a2954b4ccb4cad09da85074ef4958193fc10aea00cf33cbc8f7a309dc4d2b287a9d71e6b509b1861ddad06b0ad0508818fcd093147214cd836edd5cdf2
7
- data.tar.gz: 4d345a0068501420508694bd5491de0584517e60021ff16e216486bc8329a1c5c7655c40df33a631e280c9977f8721d4b9a572f84f60fd6cf9e449134762b768
6
+ metadata.gz: eea5b5ac599bbd4043a1caf1ff3f89d8356619f9f30a45715a8bb81628673934b58cec80b4234ff863bb41c5b1cfde33a9a00a8ceaa58ce256ec267d99cab292
7
+ data.tar.gz: 7dc7d4532c3da16a34d60e70e10022c773a2b4c748ec15efec8dda0eb3f2718f71f0f1162da28cd886e8e01138f9f945ab16bf8bcf4f4f44ebcf2a292a4a6f54
data/CHANGELOG.md CHANGED
@@ -13,13 +13,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
13
 
14
14
  ### Removed
15
15
 
16
+ ## [14.3.0]
17
+
18
+ ### Added
19
+
20
+ - Add suppport for conan parsing
21
+
22
+ ## [14.2.0]
23
+
24
+ ### Added
25
+
26
+ ### Changed
27
+
28
+ - Dependencies from yarn.lock will return a nil "type" instead of assuming "runtime".
29
+ - In Nuget .csproj files, ignored <Reference> tags that don't have a version.
30
+
31
+ ### Removed
32
+
16
33
  ## [14.1.0] - 2025-10-01
17
34
 
18
35
  ### Added
19
36
 
20
37
  ### Changed
21
38
 
22
- - Dependencies from pom.xml without a scope will not return a "type" of nil instead of guessing "runtime".
39
+ - Dependencies from pom.xml without a scope will now return a "type" of nil instead of guessing "runtime".
23
40
 
24
41
  ### Removed
25
42
 
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ported from Go code available at https://github.com/google/osv-scalibr/blob/f37275e81582aee924103d49d9a27c8e353477e7/extractor/filesystem/language/cpp/conanlock/conanlock.go
4
+ # Go code was made available under the Apache License, Version 2.0
5
+
6
+ module Bibliothecary
7
+ module Parsers
8
+ class Conan
9
+ include Bibliothecary::Analyser
10
+
11
+ def self.mapping
12
+ {
13
+ match_filename("conanfile.py") => {
14
+ kind: "manifest",
15
+ parser: :parse_conanfile_py,
16
+ },
17
+ match_filename("conanfile.txt") => {
18
+ kind: "manifest",
19
+ parser: :parse_conanfile_txt,
20
+ },
21
+ match_filename("conan.lock") => {
22
+ kind: "lockfile",
23
+ parser: :parse_lockfile,
24
+ },
25
+ }
26
+ end
27
+
28
+ add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
29
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
30
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
31
+
32
+ def self.parse_conanfile_py(file_contents, options: {})
33
+ dependencies = []
34
+
35
+ # Parse self.requires() calls in conanfile.py
36
+ # Pattern matches: self.requires("package/version") or self.requires("package/version", force=True, options={...})
37
+ # Captures only the package spec string; additional keyword arguments are ignored
38
+ file_contents.scan(/self\.requires\(\s*["']([^"']+)["']/).each do |match|
39
+ manifest_dep = match[0]
40
+ reference = parse_conan_reference(manifest_dep)
41
+
42
+ # Skip entries with no name
43
+ next if reference[:name].nil? || reference[:name].empty?
44
+
45
+ dependencies << Dependency.new(
46
+ name: reference[:name],
47
+ requirement: reference[:version],
48
+ type: "runtime",
49
+ source: options.fetch(:filename, nil),
50
+ platform: platform_name
51
+ )
52
+ end
53
+
54
+ ParserResult.new(dependencies: dependencies)
55
+ end
56
+
57
+ def self.parse_conanfile_txt(file_contents, options: {})
58
+ dependencies = []
59
+ current_section = nil
60
+
61
+ file_contents.each_line do |line|
62
+ line = line.strip
63
+
64
+ # Skip empty lines and comments
65
+ next if line.empty? || line.start_with?("#")
66
+
67
+ # Check for section headers
68
+ if line.match?(/^\[([^\]]+)\]$/)
69
+ current_section = line[1..-2]
70
+ next
71
+ end
72
+
73
+ # Parse dependencies in [requires] and [build_requires] sections
74
+ next unless %w[requires build_requires].include?(current_section)
75
+
76
+ reference = parse_conan_reference(manifest_dep)
77
+ next if reference[:name].nil? || reference[:name].empty?
78
+
79
+ dependencies << Dependency.new(
80
+ name: reference[:name],
81
+ requirement: reference[:version],
82
+ type: current_section == "requires" ? "runtime" : "development",
83
+ source: options.fetch(:filename, nil),
84
+ platform: platform_name
85
+ )
86
+ end
87
+
88
+ ParserResult.new(dependencies: dependencies)
89
+ end
90
+
91
+ def self.parse_lockfile(file_contents, options: {})
92
+ manifest = JSON.parse(file_contents)
93
+
94
+ # Auto-detect lockfile format version
95
+ if manifest.dig("graph_lock", "nodes")
96
+ parse_v1_lockfile(manifest, options: options)
97
+ else
98
+ parse_v2_lockfile(manifest, options: options)
99
+ end
100
+ end
101
+
102
+ def self.parse_v1_lockfile(lockfile, options: {})
103
+ dependencies = []
104
+
105
+ lockfile["graph_lock"]["nodes"].each_value do |node|
106
+ if node["path"] && !node["path"].empty?
107
+ # a local "conanfile.txt", skip
108
+ next
109
+ end
110
+
111
+ reference = nil
112
+ if node["pref"]
113
+ # old format 0.3 (conan 1.27-) lockfiles use "pref" instead of "ref"
114
+ reference = parse_conan_reference(node["pref"])
115
+ elsif node["ref"]
116
+ reference = parse_conan_reference(node["ref"])
117
+ else
118
+ next
119
+ end
120
+
121
+ # skip entries with no name, they are most likely consumer's conanfiles
122
+ # and not dependencies to be searched in a database anyway
123
+ next if reference[:name].nil? || reference[:name].empty?
124
+
125
+ type = case node["context"]
126
+ when "build"
127
+ "development"
128
+ else
129
+ "runtime"
130
+ end
131
+
132
+ dependencies << Dependency.new(
133
+ name: reference[:name],
134
+ requirement: reference[:version],
135
+ type: type,
136
+ source: options.fetch(:filename, nil),
137
+ platform: platform_name
138
+ )
139
+ end
140
+
141
+ ParserResult.new(dependencies: dependencies)
142
+ end
143
+
144
+ def self.parse_v2_lockfile(lockfile, options: {})
145
+ dependencies = []
146
+
147
+ parse_conan_requires(dependencies, lockfile["requires"], "runtime", options)
148
+ parse_conan_requires(dependencies, lockfile["build_requires"], "development", options)
149
+ parse_conan_requires(dependencies, lockfile["python_requires"], "development", options)
150
+
151
+ ParserResult.new(dependencies: dependencies)
152
+ end
153
+
154
+ # Helper method to parse an array of Conan package references
155
+ # Similar to OSV Scalibr's parseConanRequires function
156
+ def self.parse_conan_requires(dependencies, requires, type, options)
157
+ return unless requires && !requires.empty?
158
+
159
+ requires.each do |ref|
160
+ reference = parse_conan_reference(ref)
161
+
162
+ # Skip entries with no name, they are most likely consumer's conanfiles
163
+ # and not dependencies to be searched in a database anyway
164
+ next if reference[:name].nil? || reference[:name].empty?
165
+
166
+ dependencies << Dependency.new(
167
+ name: reference[:name],
168
+ requirement: reference[:version] || "*",
169
+ type: type,
170
+ source: options.fetch(:filename, nil),
171
+ platform: platform_name
172
+ )
173
+ end
174
+ end
175
+
176
+ # Parse Conan reference
177
+ # Handles the full Conan reference format:
178
+ # name/version[@username[/channel]][#recipe_revision][:package_id[#package_revision]][%timestamp]
179
+ #
180
+ # Based on OSV Scalibr's parseConanReference implementation:
181
+ # https://github.com/google/osv-scalibr/blob/f37275e81582aee924103d49d9a27c8e353477e7/extractor/filesystem/language/cpp/conanlock/conanlock.go
182
+ #
183
+ # Returns a hash with keys: name, version, username, channel, recipe_revision, package_id, package_revision, timestamp
184
+ def self.parse_conan_reference(ref)
185
+ reference = {
186
+ name: nil,
187
+ version: nil,
188
+ username: nil,
189
+ channel: nil,
190
+ recipe_revision: nil,
191
+ package_id: nil,
192
+ package_revision: nil,
193
+ timestamp: nil,
194
+ }
195
+
196
+ return reference if ref.nil? || ref.empty?
197
+
198
+ # Validate that ref contains "/" (name/version format)
199
+ # This filters out invalid entries like "1.2.3" (version without name)
200
+ return reference unless ref.include?("/")
201
+
202
+ # Strip timestamp: name/version%1234 -> name/version
203
+ parts = ref.split("%", 2)
204
+ if parts.length == 2
205
+ ref = parts[0]
206
+ reference[:timestamp] = parts[1]
207
+ end
208
+
209
+ # Strip package revision: name/version:pkgid#prev -> name/version
210
+ parts = ref.split(":", 2)
211
+ if parts.length == 2
212
+ ref = parts[0]
213
+ pkg_parts = parts[1].split("#", 2)
214
+ reference[:package_id] = pkg_parts[0]
215
+ reference[:package_revision] = pkg_parts[1] if pkg_parts.length == 2
216
+ end
217
+
218
+ # Strip recipe revision: name/version#rrev -> name/version
219
+ parts = ref.split("#", 2)
220
+ if parts.length == 2
221
+ ref = parts[0]
222
+ reference[:recipe_revision] = parts[1]
223
+ end
224
+
225
+ # Strip username/channel: name/version@user/channel -> name/version
226
+ parts = ref.split("@", 2)
227
+ if parts.length == 2
228
+ ref = parts[0]
229
+ user_channel = parts[1].split("/", 2)
230
+ reference[:username] = user_channel[0]
231
+ reference[:channel] = user_channel[1] if user_channel.length == 2
232
+ end
233
+
234
+ # Split name/version: name/version -> [name, version]
235
+ parts = ref.split("/", 2)
236
+ reference[:name] = parts[0]
237
+ reference[:version] = parts.length == 2 ? parts[1] : nil
238
+
239
+ reference
240
+ end
241
+ end
242
+ end
243
+ end
@@ -187,7 +187,7 @@ module Bibliothecary
187
187
  original_name: dep[:original_name],
188
188
  requirement: dep[:version],
189
189
  original_requirement: dep[:original_requirement],
190
- type: "runtime", # lockfile doesn't tell us more about the type of dep
190
+ type: nil, # yarn.lock doesn't report on the type of dependency
191
191
  local: dep[:requirements]&.first&.start_with?("file:"),
192
192
  source: options.fetch(:filename, nil),
193
193
  platform: platform_name
@@ -153,6 +153,17 @@ module Bibliothecary
153
153
  .select { |dep| dep.respond_to? "Include" }
154
154
  .map do |dependency|
155
155
  vals = *dependency.Include.split(",").map(&:strip)
156
+
157
+ # Skip <Reference> dependencies that only have the name value. Reasoning:
158
+ # Builtin assemblies like "System.Web" or "Microsoft.CSharp" can be required from the framework or by
159
+ # downloading via Nuget, and we only want to report on packages that are downloaded from Nuget. We are
160
+ # pretty sure that if they don't have a version in <Reference> then they're likely from the framework
161
+ # itself, which means they won't show up in the lockfile and we want to omit them.
162
+ # Note: if we omit a false positive here it should still show up in the lockfile, and it should be
163
+ # safer guess like this since <Reference> is an older standard.
164
+ # Note: this strategy could also skip on-disk 3rd-party packages with a <HintPath> but no version in <Reference>
165
+ next nil if vals.size == 1
166
+
156
167
  name = vals.shift
157
168
  vals = vals.to_h { |r| r.split("=", 2) }
158
169
 
@@ -164,6 +175,7 @@ module Bibliothecary
164
175
  platform: platform_name
165
176
  )
166
177
  end
178
+ .compact
167
179
 
168
180
  dependencies = packages.uniq(&:name)
169
181
  ParserResult.new(dependencies: dependencies)
@@ -12,6 +12,7 @@ module Bibliothecary
12
12
  "npm" => :npm,
13
13
  "cargo" => :cargo,
14
14
  "composer" => :packagist,
15
+ "conan" => :conan,
15
16
  "conda" => :conda,
16
17
  "cran" => :cran,
17
18
  "gem" => :rubygems,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bibliothecary
4
- VERSION = "14.1.0"
4
+ VERSION = "14.3.0"
5
5
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bibliothecary
3
3
  version: !ruby/object:Gem::Version
4
- version: 14.1.0
4
+ version: 14.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 2025-10-01 00:00:00.000000000 Z
11
+ date: 2025-11-12 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: commander
@@ -135,6 +136,7 @@ dependencies:
135
136
  - - ">="
136
137
  - !ruby/object:Gem::Version
137
138
  version: '0'
139
+ description:
138
140
  email:
139
141
  - andrewnez@gmail.com
140
142
  executables:
@@ -181,6 +183,7 @@ files:
181
183
  - lib/bibliothecary/parsers/bower.rb
182
184
  - lib/bibliothecary/parsers/cargo.rb
183
185
  - lib/bibliothecary/parsers/cocoapods.rb
186
+ - lib/bibliothecary/parsers/conan.rb
184
187
  - lib/bibliothecary/parsers/conda.rb
185
188
  - lib/bibliothecary/parsers/cpan.rb
186
189
  - lib/bibliothecary/parsers/cran.rb
@@ -209,6 +212,7 @@ licenses:
209
212
  - AGPL-3.0
210
213
  metadata:
211
214
  rubygems_mfa_required: 'true'
215
+ post_install_message:
212
216
  rdoc_options: []
213
217
  require_paths:
214
218
  - lib
@@ -223,7 +227,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
223
227
  - !ruby/object:Gem::Version
224
228
  version: '0'
225
229
  requirements: []
226
- rubygems_version: 3.6.3
230
+ rubygems_version: 3.4.19
231
+ signing_key:
227
232
  specification_version: 4
228
233
  summary: Find and parse manifests
229
234
  test_files: []