1use std::collections::HashMap;
5use std::path::Path;
6
7use crate::parser_warn as warn;
8use packageurl::PackageUrl;
9use serde_json::{Map as JsonMap, Value};
10
11use crate::models::{DatasourceId, Dependency, PackageData, PackageType, Party, PartyType};
12use crate::parsers::utils::{capped_iteration_limit, split_name_email, truncate_field};
13
14use super::PackageParser;
15
16pub struct VcpkgManifestParser;
17pub struct VcpkgLockParser;
18
19impl PackageParser for VcpkgManifestParser {
20 const PACKAGE_TYPE: PackageType = PackageType::Vcpkg;
21
22 fn is_match(path: &Path) -> bool {
23 path.file_name().and_then(|name| name.to_str()) == Some("vcpkg.json")
24 }
25
26 fn extract_packages(path: &Path) -> Vec<PackageData> {
27 let content = match crate::parsers::utils::read_file_to_string(path, None) {
28 Ok(content) => content,
29 Err(e) => {
30 warn!("Failed to read vcpkg.json at {:?}: {}", path, e);
31 return vec![default_package_data()];
32 }
33 };
34
35 let json: Value = match serde_json::from_str(&content) {
36 Ok(json) => json,
37 Err(e) => {
38 warn!("Failed to parse vcpkg.json at {:?}: {}", path, e);
39 return vec![default_package_data()];
40 }
41 };
42
43 vec![parse_vcpkg_manifest(path, &json)]
44 }
45
46 fn metadata() -> Vec<super::metadata::ParserMetadata> {
47 vec![super::metadata::ParserMetadata {
48 description: "vcpkg manifest file",
49 file_patterns: &["**/vcpkg.json"],
50 package_type: "vcpkg",
51 primary_language: "",
52 documentation_url: Some("https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-json"),
53 }]
54 }
55}
56
57impl PackageParser for VcpkgLockParser {
58 const PACKAGE_TYPE: PackageType = PackageType::Vcpkg;
59
60 fn is_match(path: &Path) -> bool {
61 path.file_name().and_then(|name| name.to_str()) == Some("vcpkg-lock.json")
62 }
63
64 fn extract_packages(path: &Path) -> Vec<PackageData> {
65 let content = match crate::parsers::utils::read_file_to_string(path, None) {
66 Ok(content) => content,
67 Err(e) => {
68 warn!("Failed to read vcpkg-lock.json at {:?}: {}", path, e);
69 return vec![default_lock_package_data()];
70 }
71 };
72
73 let json: Value = match serde_json::from_str(&content) {
74 Ok(json) => json,
75 Err(e) => {
76 warn!("Failed to parse vcpkg-lock.json at {:?}: {}", path, e);
77 return vec![default_lock_package_data()];
78 }
79 };
80
81 vec![parse_vcpkg_lock(&json)]
82 }
83
84 fn metadata() -> Vec<super::metadata::ParserMetadata> {
85 vec![super::metadata::ParserMetadata {
86 description: "vcpkg registry lockfile",
87 file_patterns: &["**/vcpkg-lock.json"],
88 package_type: "vcpkg",
89 primary_language: "C++",
90 documentation_url: Some("https://github.com/microsoft/vcpkg-tool"),
91 }]
92 }
93}
94
95fn default_package_data() -> PackageData {
96 PackageData {
97 package_type: Some(PackageType::Vcpkg),
98 datasource_id: Some(DatasourceId::VcpkgJson),
99 ..Default::default()
100 }
101}
102
103fn default_lock_package_data() -> PackageData {
104 PackageData {
105 package_type: Some(PackageType::Vcpkg),
106 datasource_id: Some(DatasourceId::VcpkgLockJson),
107 is_private: true,
108 ..Default::default()
109 }
110}
111
112fn parse_vcpkg_manifest(path: &Path, json: &Value) -> PackageData {
113 let name = get_non_empty_string(json, "name").map(truncate_field);
114 let version = manifest_version(json).map(truncate_field);
115 let description = get_string_or_array(json, "description").map(truncate_field);
116 let homepage_url = get_non_empty_string(json, "homepage").map(truncate_field);
117 let extracted_license_statement = get_string_or_array(json, "license").map(truncate_field);
118 let parties = extract_maintainers(json);
119 let dependencies = extract_dependencies(json);
120 let extra_data = build_extra_data(path, json);
121
122 PackageData {
123 package_type: Some(PackageType::Vcpkg),
124 namespace: None,
125 name: name.clone(),
126 version: version.clone(),
127 primary_language: Some("C++".to_string()),
128 description,
129 parties,
130 homepage_url,
131 extracted_license_statement,
132 is_private: name.is_none(),
133 dependencies,
134 extra_data,
135 datasource_id: Some(DatasourceId::VcpkgJson),
136 purl: name
137 .as_deref()
138 .and_then(|name| build_vcpkg_purl(name, version.as_deref()))
139 .map(truncate_field),
140 ..default_package_data()
141 }
142}
143
144fn parse_vcpkg_lock(json: &Value) -> PackageData {
145 PackageData {
146 primary_language: Some("C++".to_string()),
147 extra_data: build_lock_extra_data(json),
148 ..default_lock_package_data()
149 }
150}
151
152fn build_lock_extra_data(json: &Value) -> Option<HashMap<String, Value>> {
153 let registry_locks = extract_lock_registry_entries(json);
154 if registry_locks.is_empty() {
155 return None;
156 }
157
158 let mut extra = HashMap::new();
159 extra.insert("registry_locks".to_string(), Value::Array(registry_locks));
160 Some(extra)
161}
162
163fn extract_lock_registry_entries(json: &Value) -> Vec<Value> {
164 let Some(registries) = json.as_object() else {
165 return Vec::new();
166 };
167
168 let registry_limit = capped_iteration_limit(registries.len(), "vcpkg lock registries");
169 registries
170 .iter()
171 .take(registry_limit)
172 .filter_map(|(location, references)| {
173 let references = references.as_object()?;
174 let reference_limit = capped_iteration_limit(references.len(), "vcpkg lock references");
175 let mut normalized_references = JsonMap::new();
176 for (reference, revision) in references.iter().take(reference_limit) {
177 let Some(revision) = revision
178 .as_str()
179 .map(str::trim)
180 .filter(|revision| !revision.is_empty())
181 else {
182 continue;
183 };
184
185 normalized_references.insert(
186 truncate_field(reference.to_string()),
187 Value::String(truncate_field(revision.to_string())),
188 );
189 }
190
191 if normalized_references.is_empty() {
192 return None;
193 }
194
195 let mut entry = JsonMap::new();
196 entry.insert(
197 "location".to_string(),
198 Value::String(truncate_field(location.to_string())),
199 );
200 entry.insert(
201 "references".to_string(),
202 Value::Object(normalized_references),
203 );
204 Some(Value::Object(entry))
205 })
206 .collect()
207}
208
209fn manifest_version(json: &Value) -> Option<String> {
210 let version = [
211 "version",
212 "version-semver",
213 "version-date",
214 "version-string",
215 ]
216 .into_iter()
217 .find_map(|field| get_non_empty_string(json, field));
218
219 match (version, json.get("port-version").and_then(Value::as_i64)) {
220 (Some(version), Some(port_version)) if port_version > 0 => {
221 Some(format!("{}#{}", version, port_version))
222 }
223 (version, _) => version,
224 }
225}
226
227fn extract_maintainers(json: &Value) -> Vec<Party> {
228 let Some(value) = json.get("maintainers") else {
229 return Vec::new();
230 };
231
232 let maintainers: Vec<String> = match value {
233 Value::String(s) => vec![s.clone()],
234 Value::Array(values) => {
235 let limit = capped_iteration_limit(values.len(), "vcpkg maintainers");
236 values
237 .iter()
238 .take(limit)
239 .filter_map(Value::as_str)
240 .map(ToOwned::to_owned)
241 .collect()
242 }
243 _ => Vec::new(),
244 };
245
246 maintainers
247 .into_iter()
248 .map(|entry| {
249 let (name, email) = split_name_email(&entry);
250 Party {
251 r#type: Some(PartyType::Person),
252 role: Some("maintainer".to_string()),
253 name: name.map(truncate_field),
254 email: email.map(truncate_field),
255 url: None,
256 organization: None,
257 organization_url: None,
258 timezone: None,
259 }
260 })
261 .collect()
262}
263
264fn extract_dependencies(json: &Value) -> Vec<Dependency> {
265 let mut dependencies: Vec<Dependency> = json
266 .get("dependencies")
267 .and_then(Value::as_array)
268 .map(|deps| {
269 let limit = capped_iteration_limit(deps.len(), "vcpkg dependencies");
270 deps.iter()
271 .take(limit)
272 .filter_map(parse_dependency_entry)
273 .collect()
274 })
275 .unwrap_or_default();
276
277 if let Some(features) = json.get("features").and_then(Value::as_object) {
278 let features_limit = capped_iteration_limit(features.len(), "vcpkg features");
279 for (feature_name, feature_value) in features.iter().take(features_limit) {
280 let Some(feature_dependencies) =
281 feature_value.get("dependencies").and_then(Value::as_array)
282 else {
283 continue;
284 };
285
286 let feature_deps_limit =
287 capped_iteration_limit(feature_dependencies.len(), "vcpkg feature dependencies");
288 for dependency in feature_dependencies
289 .iter()
290 .take(feature_deps_limit)
291 .filter_map(parse_dependency_entry)
292 .map(|mut dependency| {
293 let mut extra_data = dependency.extra_data.take().unwrap_or_default();
294 extra_data.insert(
295 "feature".to_string(),
296 Value::String(feature_name.to_string()),
297 );
298 dependency.extra_data = Some(extra_data);
299 dependency
300 })
301 {
302 dependencies.push(dependency);
303 }
304 }
305 }
306
307 dependencies
308}
309
310fn parse_dependency_entry(value: &Value) -> Option<Dependency> {
311 match value {
312 Value::String(name) => Some(Dependency {
313 purl: build_vcpkg_purl(name, None).map(truncate_field),
314 extracted_requirement: Some(truncate_field(name.clone())),
315 scope: Some("dependencies".to_string()),
316 is_runtime: Some(true),
317 is_optional: Some(false),
318 is_pinned: Some(false),
319 is_direct: Some(true),
320 resolved_package: None,
321 extra_data: None,
322 }),
323 Value::Object(obj) => {
324 let name = obj.get("name").and_then(Value::as_str)?.trim();
325 if name.is_empty() {
326 return None;
327 }
328
329 let extracted_requirement = obj
330 .get("version>=")
331 .and_then(Value::as_str)
332 .map(|v| truncate_field(v.to_owned()))
333 .or_else(|| Some(truncate_field(name.to_string())));
334
335 let host = obj.get("host").and_then(Value::as_bool).unwrap_or(false);
336 let mut extra = HashMap::new();
337 for field in [
338 "version>=",
339 "features",
340 "default-features",
341 "host",
342 "platform",
343 ] {
344 if let Some(field_value) = obj.get(field) {
345 extra.insert(field.to_string(), field_value.clone());
346 }
347 }
348
349 Some(Dependency {
350 purl: build_vcpkg_purl(name, None).map(truncate_field),
351 extracted_requirement,
352 scope: Some("dependencies".to_string()),
353 is_runtime: Some(!host),
354 is_optional: Some(false),
355 is_pinned: Some(false),
356 is_direct: Some(true),
357 resolved_package: None,
358 extra_data: (!extra.is_empty()).then_some(extra),
359 })
360 }
361 _ => None,
362 }
363}
364
365fn build_extra_data(path: &Path, json: &Value) -> Option<HashMap<String, Value>> {
366 let mut extra = HashMap::new();
367 for field in [
368 "builtin-baseline",
369 "overrides",
370 "supports",
371 "default-features",
372 "features",
373 "configuration",
374 "vcpkg-configuration",
375 "documentation",
376 ] {
377 if let Some(value) = json.get(field) {
378 extra.insert(field.to_string(), value.clone());
379 }
380 }
381
382 if !extra.contains_key("configuration")
383 && !extra.contains_key("vcpkg-configuration")
384 && let Some(config) = read_sibling_configuration(path)
385 {
386 extra.insert("configuration".to_string(), config);
387 }
388
389 (!extra.is_empty()).then_some(extra)
390}
391
392fn read_sibling_configuration(path: &Path) -> Option<Value> {
393 let sibling_path = path.with_file_name("vcpkg-configuration.json");
394 let content = crate::parsers::utils::read_file_to_string(&sibling_path, None).ok()?;
395 match serde_json::from_str(&content) {
396 Ok(value) => Some(value),
397 Err(e) => {
398 warn!(
399 "Failed to parse sibling vcpkg-configuration.json at {:?}: {}",
400 sibling_path, e
401 );
402 None
403 }
404 }
405}
406
407fn get_non_empty_string(json: &Value, field: &str) -> Option<String> {
408 json.get(field)
409 .and_then(Value::as_str)
410 .map(str::trim)
411 .filter(|value| !value.is_empty())
412 .map(|value| value.to_string())
413}
414
415fn get_string_or_array(json: &Value, field: &str) -> Option<String> {
416 match json.get(field) {
417 Some(Value::String(s)) if !s.trim().is_empty() => Some(s.trim().to_string()),
418 Some(Value::Array(values)) => {
419 let collected: Vec<_> = values
420 .iter()
421 .filter_map(Value::as_str)
422 .map(str::trim)
423 .filter(|s| !s.is_empty())
424 .collect();
425 (!collected.is_empty()).then(|| collected.join("\n"))
426 }
427 _ => None,
428 }
429}
430
431fn build_vcpkg_purl(name: &str, version: Option<&str>) -> Option<String> {
432 let mut purl = PackageUrl::new("generic", name).ok()?;
433 purl.with_namespace("vcpkg").ok()?;
434 if let Some(version) = version {
435 purl.with_version(version).ok()?;
436 }
437 Some(purl.to_string())
438}