bibliothecary 14.2.0 → 14.4.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: 548767337e8898f61951f3e458310fd5299d88c81549a3332750c62427b93a28
4
- data.tar.gz: 660e80fcdce7b4fc1ed9d498b4a8e39856498e709b16fbd0f3ffcdc5671a8942
3
+ metadata.gz: d1f7fd4aeb8298f9fcb9b5edc69b7936eed8f112f3a6dde000edda36d641cfca
4
+ data.tar.gz: 4d9b619aab4813b3ee77a36b0fbb1588b561a3dd42275feeb216f01d19f8ea05
5
5
  SHA512:
6
- metadata.gz: 67d3cf0c77840be64458f50dac795a8a782e192db0138e316e21ef65fb9dd625a32fde8e5ef92fa4e47ce66bb666d5fd97d751930ad6eea4bd7a404836c03698
7
- data.tar.gz: 4490815f18aac91bfa307349272699c16500f3452d88b0681ce2c927e4b7f6acc55e41afbb33299cfaa5d4330306c34b30598a42b092a732097b617de1f378a9
6
+ metadata.gz: a9a904ac75104ea34f45bfea2900475c89fd29a5d15ac065923d4e9d4c715ec6380d92ecea6499025063abe8f0146d2a89d9661f59f98cd216a4dcc6ce44b054
7
+ data.tar.gz: a4234dc007565e41587fa92ca7f970e5fdffd479debd202658e8a04edcf01943624df11aa1d0785398b87d10f694f3b84fb68fb985251315fbff67799f24fa2f
data/CHANGELOG.md CHANGED
@@ -13,6 +13,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
13
 
14
14
  ### Removed
15
15
 
16
+ ## [14.4.0]
17
+
18
+ ### Added
19
+
20
+ - Add suppport for vcpkg parsing
21
+
22
+ ## [14.3.0]
23
+
24
+ ### Added
25
+
26
+ - Add suppport for conan parsing
27
+
16
28
  ## [14.2.0]
17
29
 
18
30
  ### Added
@@ -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
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bibliothecary
4
+ module Parsers
5
+ class Vcpkg
6
+ include Bibliothecary::Analyser
7
+
8
+ def self.mapping
9
+ {
10
+ match_filename("vcpkg.json") => {
11
+ kind: "manifest",
12
+ parser: :parse_vcpkg_json,
13
+ },
14
+ # _generated-vcpkg-list.json is the output of `vcpkg list --x-json`.
15
+ match_filename("_generated-vcpkg-list.json") => {
16
+ kind: "lockfile",
17
+ parser: :parse_vcpkg_list_json,
18
+ },
19
+ }
20
+ end
21
+
22
+ add_multi_parser(Bibliothecary::MultiParsers::CycloneDX)
23
+ add_multi_parser(Bibliothecary::MultiParsers::Spdx)
24
+ add_multi_parser(Bibliothecary::MultiParsers::DependenciesCSV)
25
+
26
+ def self.parse_vcpkg_json(file_contents, options: {})
27
+ dependencies = []
28
+ manifest = JSON.parse(file_contents)
29
+ deps = manifest["dependencies"]
30
+
31
+ if !deps || deps.empty?
32
+ return ParserResult.new(dependencies: dependencies)
33
+ end
34
+
35
+ overrides = {}
36
+ manifest["overrides"]&.each do |override|
37
+ if override.is_a?(Hash) && override["name"]
38
+ override_version = override["version"] || override["version-semver"] || override["version-date"] || override["version-string"]
39
+ overrides[override["name"]] = format_requirement(override_version, override["port-version"])
40
+ end
41
+ end
42
+
43
+ deps.each do |dep|
44
+ if dep.is_a?(String)
45
+ # Simple string format: "boost-system"
46
+ name = dep
47
+ requirement = nil
48
+ is_development = false
49
+ elsif dep.is_a?(Hash)
50
+ # Object format: { "name": "cpprestsdk", "version>=": "2.10.0", ... }
51
+ name = dep["name"]
52
+ requirement = if dep["version>="]
53
+ ">=#{dep['version>=']}"
54
+ end
55
+ is_development = dep["host"] == true
56
+ end
57
+
58
+ # Skip entries with no name
59
+ next if name.nil? || name.empty?
60
+
61
+ requirement = overrides[name] if overrides[name]
62
+
63
+ dependencies << Dependency.new(
64
+ platform: platform_name,
65
+ name: name,
66
+ requirement: requirement,
67
+ type: is_development ? "development" : "runtime",
68
+ source: options.fetch(:filename, nil)
69
+ )
70
+ end
71
+
72
+ ParserResult.new(dependencies: dependencies)
73
+ end
74
+
75
+ def self.parse_vcpkg_list_json(file_contents, options: {})
76
+ # parses the output of `vcpkg list --x-json`
77
+ dependencies = []
78
+ manifest = JSON.parse(file_contents)
79
+
80
+ manifest.each_value do |package_info|
81
+ name = package_info["package_name"]
82
+ version = package_info["version"]
83
+ port_version = package_info["port_version"]
84
+
85
+ # Skip entries with no name
86
+ next if name.nil? || name.empty?
87
+
88
+ dependencies << Dependency.new(
89
+ platform: platform_name,
90
+ name: name,
91
+ requirement: format_requirement(version, port_version),
92
+ type: "runtime",
93
+ source: options.fetch(:filename, nil)
94
+ )
95
+ end
96
+
97
+ ParserResult.new(dependencies: dependencies)
98
+ end
99
+
100
+ def self.format_requirement(version, port_version)
101
+ return "*" unless version
102
+
103
+ if port_version && port_version > 0
104
+ return "#{version}##{port_version}"
105
+ end
106
+
107
+ version
108
+ end
109
+ end
110
+ end
111
+ end
@@ -12,11 +12,13 @@ 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,
18
19
  "nuget" => :nuget,
19
20
  "pypi" => :pypi,
21
+ "vcpkg" => :vcpkg,
20
22
  }.freeze
21
23
 
22
24
  # @param purl [PackageURL]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bibliothecary
4
- VERSION = "14.2.0"
4
+ VERSION = "14.4.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.2.0
4
+ version: 14.4.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-02 00:00:00.000000000 Z
11
+ date: 2025-11-24 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
@@ -198,6 +201,7 @@ files:
198
201
  - lib/bibliothecary/parsers/pypi.rb
199
202
  - lib/bibliothecary/parsers/rubygems.rb
200
203
  - lib/bibliothecary/parsers/shard.rb
204
+ - lib/bibliothecary/parsers/vcpkg.rb
201
205
  - lib/bibliothecary/purl_util.rb
202
206
  - lib/bibliothecary/related_files_info.rb
203
207
  - lib/bibliothecary/runner.rb
@@ -209,6 +213,7 @@ licenses:
209
213
  - AGPL-3.0
210
214
  metadata:
211
215
  rubygems_mfa_required: 'true'
216
+ post_install_message:
212
217
  rdoc_options: []
213
218
  require_paths:
214
219
  - lib
@@ -223,7 +228,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
223
228
  - !ruby/object:Gem::Version
224
229
  version: '0'
225
230
  requirements: []
226
- rubygems_version: 3.6.3
231
+ rubygems_version: 3.4.19
232
+ signing_key:
227
233
  specification_version: 4
228
234
  summary: Find and parse manifests
229
235
  test_files: []