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::rfc822::{self, Rfc822Metadata};
13use crate::parsers::utils::{capped_iteration_limit, split_name_email, truncate_field};
14
15use super::PackageParser;
16
17pub struct VcpkgManifestParser;
18pub struct VcpkgConfigurationParser;
19pub struct VcpkgLockParser;
20pub struct VcpkgControlParser;
21
22impl PackageParser for VcpkgManifestParser {
23 const PACKAGE_TYPE: PackageType = PackageType::Vcpkg;
24
25 fn is_match(path: &Path) -> bool {
26 path.file_name().and_then(|name| name.to_str()) == Some("vcpkg.json")
27 }
28
29 fn extract_packages(path: &Path) -> Vec<PackageData> {
30 let content = match crate::parsers::utils::read_file_to_string(path, None) {
31 Ok(content) => content,
32 Err(e) => {
33 warn!("Failed to read vcpkg.json at {:?}: {}", path, e);
34 return vec![default_package_data()];
35 }
36 };
37
38 let json: Value = match serde_json::from_str(&content) {
39 Ok(json) => json,
40 Err(e) => {
41 warn!("Failed to parse vcpkg.json at {:?}: {}", path, e);
42 return vec![default_package_data()];
43 }
44 };
45
46 vec![parse_vcpkg_manifest(path, &json)]
47 }
48
49 fn metadata() -> Vec<super::metadata::ParserMetadata> {
50 vec![super::metadata::ParserMetadata {
51 description: "vcpkg manifest file",
52 file_patterns: &["**/vcpkg.json"],
53 package_type: "vcpkg",
54 primary_language: "",
55 documentation_url: Some("https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-json"),
56 }]
57 }
58}
59
60impl PackageParser for VcpkgConfigurationParser {
61 const PACKAGE_TYPE: PackageType = PackageType::Vcpkg;
62
63 fn is_match(path: &Path) -> bool {
64 path.file_name().and_then(|name| name.to_str()) == Some("vcpkg-configuration.json")
65 }
66
67 fn extract_packages(path: &Path) -> Vec<PackageData> {
68 let content = match crate::parsers::utils::read_file_to_string(path, None) {
69 Ok(content) => content,
70 Err(e) => {
71 warn!(
72 "Failed to read vcpkg-configuration.json at {:?}: {}",
73 path, e
74 );
75 return vec![default_configuration_package_data()];
76 }
77 };
78
79 let json: Value = match serde_json::from_str(&content) {
80 Ok(json) => json,
81 Err(e) => {
82 warn!(
83 "Failed to parse vcpkg-configuration.json at {:?}: {}",
84 path, e
85 );
86 return vec![default_configuration_package_data()];
87 }
88 };
89
90 vec![parse_vcpkg_configuration(&json)]
91 }
92
93 fn metadata() -> Vec<super::metadata::ParserMetadata> {
94 vec![super::metadata::ParserMetadata {
95 description: "vcpkg configuration file",
96 file_patterns: &["**/vcpkg-configuration.json"],
97 package_type: "vcpkg",
98 primary_language: "C++",
99 documentation_url: Some(
100 "https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-configuration-json",
101 ),
102 }]
103 }
104}
105
106impl PackageParser for VcpkgLockParser {
107 const PACKAGE_TYPE: PackageType = PackageType::Vcpkg;
108
109 fn is_match(path: &Path) -> bool {
110 path.file_name().and_then(|name| name.to_str()) == Some("vcpkg-lock.json")
111 }
112
113 fn extract_packages(path: &Path) -> Vec<PackageData> {
114 let content = match crate::parsers::utils::read_file_to_string(path, None) {
115 Ok(content) => content,
116 Err(e) => {
117 warn!("Failed to read vcpkg-lock.json at {:?}: {}", path, e);
118 return vec![default_lock_package_data()];
119 }
120 };
121
122 let json: Value = match serde_json::from_str(&content) {
123 Ok(json) => json,
124 Err(e) => {
125 warn!("Failed to parse vcpkg-lock.json at {:?}: {}", path, e);
126 return vec![default_lock_package_data()];
127 }
128 };
129
130 vec![parse_vcpkg_lock(&json)]
131 }
132
133 fn metadata() -> Vec<super::metadata::ParserMetadata> {
134 vec![super::metadata::ParserMetadata {
135 description: "vcpkg registry lockfile",
136 file_patterns: &["**/vcpkg-lock.json"],
137 package_type: "vcpkg",
138 primary_language: "C++",
139 documentation_url: Some("https://github.com/microsoft/vcpkg-tool"),
140 }]
141 }
142}
143
144impl PackageParser for VcpkgControlParser {
145 const PACKAGE_TYPE: PackageType = PackageType::Vcpkg;
146
147 fn is_match(path: &Path) -> bool {
148 path.file_name().and_then(|name| name.to_str()) == Some("CONTROL")
149 && path
150 .parent()
151 .and_then(Path::parent)
152 .and_then(|parent| parent.file_name())
153 .and_then(|name| name.to_str())
154 == Some("ports")
155 }
156
157 fn extract_packages(path: &Path) -> Vec<PackageData> {
158 let content = match crate::parsers::utils::read_file_to_string(path, None) {
159 Ok(content) => content,
160 Err(e) => {
161 warn!("Failed to read vcpkg CONTROL at {:?}: {}", path, e);
162 return vec![default_control_package_data()];
163 }
164 };
165
166 vec![parse_vcpkg_control(&content)]
167 }
168
169 fn metadata() -> Vec<super::metadata::ParserMetadata> {
170 vec![super::metadata::ParserMetadata {
171 description: "vcpkg classic port CONTROL file",
172 file_patterns: &["**/ports/*/CONTROL"],
173 package_type: "vcpkg",
174 primary_language: "C++",
175 documentation_url: Some(
176 "https://learn.microsoft.com/en-us/vcpkg/maintainers/control-files",
177 ),
178 }]
179 }
180}
181
182fn default_package_data() -> PackageData {
183 PackageData {
184 package_type: Some(PackageType::Vcpkg),
185 datasource_id: Some(DatasourceId::VcpkgJson),
186 ..Default::default()
187 }
188}
189
190fn default_lock_package_data() -> PackageData {
191 PackageData {
192 package_type: Some(PackageType::Vcpkg),
193 datasource_id: Some(DatasourceId::VcpkgLockJson),
194 is_private: true,
195 ..Default::default()
196 }
197}
198
199fn default_configuration_package_data() -> PackageData {
200 PackageData {
201 package_type: Some(PackageType::Vcpkg),
202 datasource_id: Some(DatasourceId::VcpkgConfigurationJson),
203 is_private: true,
204 ..Default::default()
205 }
206}
207
208fn default_control_package_data() -> PackageData {
209 PackageData {
210 package_type: Some(PackageType::Vcpkg),
211 datasource_id: Some(DatasourceId::VcpkgControl),
212 primary_language: Some("C++".to_string()),
213 is_private: true,
214 ..Default::default()
215 }
216}
217
218fn parse_vcpkg_manifest(path: &Path, json: &Value) -> PackageData {
219 let name = get_non_empty_string(json, "name").map(truncate_field);
220 let version = manifest_version(json).map(truncate_field);
221 let description = get_string_or_array(json, "description").map(truncate_field);
222 let homepage_url = get_non_empty_string(json, "homepage").map(truncate_field);
223 let extracted_license_statement = get_string_or_array(json, "license").map(truncate_field);
224 let parties = extract_maintainers(json);
225 let dependencies = extract_dependencies(json);
226 let extra_data = build_extra_data(path, json);
227
228 PackageData {
229 package_type: Some(PackageType::Vcpkg),
230 namespace: None,
231 name: name.clone(),
232 version: version.clone(),
233 primary_language: Some("C++".to_string()),
234 description,
235 parties,
236 homepage_url,
237 extracted_license_statement,
238 is_private: name.is_none(),
239 dependencies,
240 extra_data,
241 datasource_id: Some(DatasourceId::VcpkgJson),
242 purl: name
243 .as_deref()
244 .and_then(|name| build_vcpkg_purl(name, version.as_deref()))
245 .map(truncate_field),
246 ..default_package_data()
247 }
248}
249
250fn parse_vcpkg_lock(json: &Value) -> PackageData {
251 PackageData {
252 primary_language: Some("C++".to_string()),
253 extra_data: build_lock_extra_data(json),
254 ..default_lock_package_data()
255 }
256}
257
258fn parse_vcpkg_configuration(json: &Value) -> PackageData {
259 PackageData {
260 primary_language: Some("C++".to_string()),
261 extra_data: build_configuration_extra_data(json),
262 ..default_configuration_package_data()
263 }
264}
265
266fn parse_vcpkg_control(content: &str) -> PackageData {
267 let paragraphs = rfc822::parse_rfc822_paragraphs(content);
268 let Some(source_paragraph) = paragraphs.first() else {
269 return default_control_package_data();
270 };
271
272 let Some(name) = rfc822::get_header_first(&source_paragraph.headers, "source")
273 .map(truncate_field)
274 .filter(|name| !name.is_empty())
275 else {
276 return default_control_package_data();
277 };
278
279 let version = control_version(source_paragraph).map(truncate_field);
280 let description = rfc822::get_header_first(&source_paragraph.headers, "description")
281 .map(truncate_field)
282 .filter(|description| !description.is_empty());
283 let homepage_url =
284 rfc822::get_header_first(&source_paragraph.headers, "homepage").map(truncate_field);
285
286 let feature_paragraphs = ¶graphs[1..];
287 let feature_limit = capped_iteration_limit(
288 feature_paragraphs.len(),
289 "vcpkg CONTROL feature dependency paragraphs",
290 );
291
292 let mut dependencies = extract_control_dependencies(source_paragraph, None);
293 for feature_paragraph in feature_paragraphs.iter().take(feature_limit) {
294 if let Some(feature) = rfc822::get_header_first(&feature_paragraph.headers, "feature")
295 .map(truncate_field)
296 .filter(|feature| !feature.is_empty())
297 {
298 dependencies.extend(extract_control_dependencies(
299 feature_paragraph,
300 Some(feature.as_str()),
301 ));
302 }
303 }
304
305 PackageData {
306 package_type: Some(PackageType::Vcpkg),
307 name: Some(name.clone()),
308 version: version.clone(),
309 primary_language: Some("C++".to_string()),
310 description,
311 homepage_url,
312 dependencies,
313 extra_data: build_control_extra_data(source_paragraph, feature_paragraphs),
314 datasource_id: Some(DatasourceId::VcpkgControl),
315 purl: build_vcpkg_purl(&name, version.as_deref()).map(truncate_field),
316 is_private: false,
317 ..default_control_package_data()
318 }
319}
320
321fn control_version(paragraph: &Rfc822Metadata) -> Option<String> {
322 let version = rfc822::get_header_first(¶graph.headers, "version")?;
323 let port_version = rfc822::get_header_first(¶graph.headers, "port-version")
324 .and_then(|value| value.parse::<u64>().ok())
325 .filter(|port_version| *port_version > 0);
326
327 match port_version {
328 Some(port_version) => Some(format!("{}#{}", version, port_version)),
329 None => Some(version),
330 }
331}
332
333fn build_control_extra_data(
334 source_paragraph: &Rfc822Metadata,
335 feature_paragraphs: &[Rfc822Metadata],
336) -> Option<HashMap<String, Value>> {
337 let mut extra = HashMap::new();
338
339 if let Some(default_features) =
340 rfc822::get_header_first(&source_paragraph.headers, "default-features")
341 {
342 let features =
343 string_list_to_json_array(&default_features, "vcpkg CONTROL default features");
344 if !features.is_empty() {
345 extra.insert("default-features".to_string(), Value::Array(features));
346 }
347 }
348
349 if let Some(supports) = rfc822::get_header_first(&source_paragraph.headers, "supports")
350 .filter(|supports| !supports.is_empty())
351 {
352 extra.insert(
353 "supports".to_string(),
354 Value::String(truncate_field(supports)),
355 );
356 }
357
358 let features = extract_control_feature_metadata(feature_paragraphs);
359 if !features.is_empty() {
360 extra.insert("features".to_string(), Value::Array(features));
361 }
362
363 (!extra.is_empty()).then_some(extra)
364}
365
366fn extract_control_feature_metadata(feature_paragraphs: &[Rfc822Metadata]) -> Vec<Value> {
367 let feature_limit =
368 capped_iteration_limit(feature_paragraphs.len(), "vcpkg CONTROL feature paragraphs");
369 feature_paragraphs
370 .iter()
371 .take(feature_limit)
372 .filter_map(|paragraph| {
373 let name = rfc822::get_header_first(¶graph.headers, "feature")
374 .map(truncate_field)
375 .filter(|name| !name.is_empty())?;
376
377 let mut feature = JsonMap::new();
378 feature.insert("name".to_string(), Value::String(name));
379 if let Some(description) = rfc822::get_header_first(¶graph.headers, "description")
380 .map(truncate_field)
381 .filter(|description| !description.is_empty())
382 {
383 feature.insert("description".to_string(), Value::String(description));
384 }
385 if let Some(build_depends) =
386 rfc822::get_header_first(¶graph.headers, "build-depends")
387 {
388 let dependencies =
389 string_list_to_json_array(&build_depends, "vcpkg CONTROL feature dependencies");
390 if !dependencies.is_empty() {
391 feature.insert("build-depends".to_string(), Value::Array(dependencies));
392 }
393 }
394
395 Some(Value::Object(feature))
396 })
397 .collect()
398}
399
400fn extract_control_dependencies(
401 paragraph: &Rfc822Metadata,
402 feature_name: Option<&str>,
403) -> Vec<Dependency> {
404 let Some(build_depends) = rfc822::get_header_first(¶graph.headers, "build-depends") else {
405 return Vec::new();
406 };
407
408 split_control_list(&build_depends, "vcpkg CONTROL dependencies")
409 .into_iter()
410 .filter_map(|dependency| parse_control_dependency_entry(&dependency, feature_name))
411 .collect()
412}
413
414fn parse_control_dependency_entry(entry: &str, feature_name: Option<&str>) -> Option<Dependency> {
415 let raw_entry = entry.trim();
416 if raw_entry.is_empty() {
417 return None;
418 }
419
420 let (without_platform, platform) = split_dependency_platform(raw_entry);
421 let (name, features) = split_dependency_features(without_platform);
422 let name = name.trim();
423 if name.is_empty() {
424 return None;
425 }
426
427 let mut extra_data = HashMap::new();
428 if !features.is_empty() {
429 extra_data.insert("features".to_string(), Value::Array(features));
430 }
431 if let Some(platform) = platform {
432 extra_data.insert(
433 "platform".to_string(),
434 Value::String(truncate_field(platform)),
435 );
436 }
437 if let Some(feature_name) = feature_name {
438 extra_data.insert(
439 "feature".to_string(),
440 Value::String(truncate_field(feature_name.to_string())),
441 );
442 }
443 let is_optional = if feature_name.is_some() {
444 None
445 } else {
446 Some(false)
447 };
448
449 Some(Dependency {
450 purl: build_vcpkg_purl(name, None).map(truncate_field),
451 extracted_requirement: Some(truncate_field(raw_entry.to_string())),
452 scope: Some("build-depends".to_string()),
453 is_runtime: Some(true),
454 is_optional,
455 is_pinned: Some(false),
456 is_direct: Some(true),
457 resolved_package: None,
458 extra_data: (!extra_data.is_empty()).then_some(extra_data),
459 })
460}
461
462fn split_control_list(value: &str, context: &str) -> Vec<String> {
463 let mut entries = Vec::new();
464 let mut start = 0;
465 let mut bracket_depth = 0_u32;
466 let mut paren_depth = 0_u32;
467
468 for (index, ch) in value.char_indices() {
469 match ch {
470 '[' => bracket_depth += 1,
471 ']' => bracket_depth = bracket_depth.saturating_sub(1),
472 '(' => paren_depth += 1,
473 ')' => paren_depth = paren_depth.saturating_sub(1),
474 ',' if bracket_depth == 0 && paren_depth == 0 => {
475 entries.push(value[start..index].trim().to_string());
476 start = index + ch.len_utf8();
477 }
478 _ => {}
479 }
480 }
481 entries.push(value[start..].trim().to_string());
482
483 let limit = capped_iteration_limit(entries.len(), context);
484 entries
485 .into_iter()
486 .take(limit)
487 .filter(|entry| !entry.is_empty())
488 .collect()
489}
490
491fn string_list_to_json_array(value: &str, context: &str) -> Vec<Value> {
492 split_control_list(value, context)
493 .into_iter()
494 .map(|entry| Value::String(truncate_field(entry)))
495 .collect()
496}
497
498fn split_dependency_platform(entry: &str) -> (&str, Option<String>) {
499 let entry = entry.trim();
500 if !entry.ends_with(')') {
501 return (entry, None);
502 }
503
504 let mut depth = 0_i32;
505 for (index, ch) in entry.char_indices().rev() {
506 match ch {
507 ')' => depth += 1,
508 '(' => {
509 depth -= 1;
510 if depth == 0 {
511 let dependency = entry[..index].trim_end();
512 let platform = entry[index + 1..entry.len() - 1].trim();
513 if dependency.is_empty() || platform.is_empty() {
514 return (entry, None);
515 }
516 return (dependency, Some(platform.to_string()));
517 }
518 }
519 _ => {}
520 }
521 }
522
523 (entry, None)
524}
525
526fn split_dependency_features(entry: &str) -> (&str, Vec<Value>) {
527 let entry = entry.trim();
528 let Some(start) = entry.find('[') else {
529 return (entry, Vec::new());
530 };
531 if !entry.ends_with(']') {
532 return (entry, Vec::new());
533 }
534
535 let name = entry[..start].trim();
536 let mut features = Vec::new();
537 let mut remaining = entry[start..].trim();
538 while !remaining.is_empty() {
539 let Some(after_open) = remaining.strip_prefix('[') else {
540 return (entry, Vec::new());
541 };
542 let Some(end) = after_open.find(']') else {
543 return (entry, Vec::new());
544 };
545
546 features.extend(
547 after_open[..end]
548 .split(',')
549 .map(str::trim)
550 .filter(|feature| !feature.is_empty())
551 .map(|feature| Value::String(truncate_field(feature.to_string()))),
552 );
553 remaining = after_open[end + 1..].trim_start();
554 }
555
556 (name, features)
557}
558
559fn build_configuration_extra_data(json: &Value) -> Option<HashMap<String, Value>> {
564 let mut extra = HashMap::new();
565 for field in [
566 "default-registry",
567 "registries",
568 "overlay-ports",
569 "overlay-triplets",
570 ] {
571 if let Some(value) = json.get(field) {
572 extra.insert(field.to_string(), value.clone());
573 }
574 }
575
576 (!extra.is_empty()).then_some(extra)
577}
578
579fn build_lock_extra_data(json: &Value) -> Option<HashMap<String, Value>> {
580 let registry_locks = extract_lock_registry_entries(json);
581 if registry_locks.is_empty() {
582 return None;
583 }
584
585 let mut extra = HashMap::new();
586 extra.insert("registry_locks".to_string(), Value::Array(registry_locks));
587 Some(extra)
588}
589
590fn extract_lock_registry_entries(json: &Value) -> Vec<Value> {
591 let Some(registries) = json.as_object() else {
592 return Vec::new();
593 };
594
595 let registry_limit = capped_iteration_limit(registries.len(), "vcpkg lock registries");
596 registries
597 .iter()
598 .take(registry_limit)
599 .filter_map(|(location, references)| {
600 let references = references.as_object()?;
601 let reference_limit = capped_iteration_limit(references.len(), "vcpkg lock references");
602 let mut normalized_references = JsonMap::new();
603 for (reference, revision) in references.iter().take(reference_limit) {
604 let Some(revision) = revision
605 .as_str()
606 .map(str::trim)
607 .filter(|revision| !revision.is_empty())
608 else {
609 continue;
610 };
611
612 normalized_references.insert(
613 truncate_field(reference.to_string()),
614 Value::String(truncate_field(revision.to_string())),
615 );
616 }
617
618 if normalized_references.is_empty() {
619 return None;
620 }
621
622 let mut entry = JsonMap::new();
623 entry.insert(
624 "location".to_string(),
625 Value::String(truncate_field(location.to_string())),
626 );
627 entry.insert(
628 "references".to_string(),
629 Value::Object(normalized_references),
630 );
631 Some(Value::Object(entry))
632 })
633 .collect()
634}
635
636fn manifest_version(json: &Value) -> Option<String> {
637 let version = [
638 "version",
639 "version-semver",
640 "version-date",
641 "version-string",
642 ]
643 .into_iter()
644 .find_map(|field| get_non_empty_string(json, field));
645
646 match (version, json.get("port-version").and_then(Value::as_i64)) {
647 (Some(version), Some(port_version)) if port_version > 0 => {
648 Some(format!("{}#{}", version, port_version))
649 }
650 (version, _) => version,
651 }
652}
653
654fn extract_maintainers(json: &Value) -> Vec<Party> {
655 let Some(value) = json.get("maintainers") else {
656 return Vec::new();
657 };
658
659 let maintainers: Vec<String> = match value {
660 Value::String(s) => vec![s.clone()],
661 Value::Array(values) => {
662 let limit = capped_iteration_limit(values.len(), "vcpkg maintainers");
663 values
664 .iter()
665 .take(limit)
666 .filter_map(Value::as_str)
667 .map(ToOwned::to_owned)
668 .collect()
669 }
670 _ => Vec::new(),
671 };
672
673 maintainers
674 .into_iter()
675 .map(|entry| {
676 let (name, email) = split_name_email(&entry);
677 Party {
678 r#type: Some(PartyType::Person),
679 role: Some("maintainer".to_string()),
680 name: name.map(truncate_field),
681 email: email.map(truncate_field),
682 url: None,
683 organization: None,
684 organization_url: None,
685 timezone: None,
686 }
687 })
688 .collect()
689}
690
691fn extract_dependencies(json: &Value) -> Vec<Dependency> {
692 let overrides = extract_overrides(json);
693
694 let mut dependencies: Vec<Dependency> = json
695 .get("dependencies")
696 .and_then(Value::as_array)
697 .map(|deps| {
698 let limit = capped_iteration_limit(deps.len(), "vcpkg dependencies");
699 deps.iter()
700 .take(limit)
701 .filter_map(|dep| parse_dependency_entry(dep, &overrides))
702 .collect()
703 })
704 .unwrap_or_default();
705
706 if let Some(features) = json.get("features").and_then(Value::as_object) {
707 let features_limit = capped_iteration_limit(features.len(), "vcpkg features");
708 for (feature_name, feature_value) in features.iter().take(features_limit) {
709 let Some(feature_dependencies) =
710 feature_value.get("dependencies").and_then(Value::as_array)
711 else {
712 continue;
713 };
714
715 let feature_deps_limit =
716 capped_iteration_limit(feature_dependencies.len(), "vcpkg feature dependencies");
717 for dependency in feature_dependencies
718 .iter()
719 .take(feature_deps_limit)
720 .filter_map(|dep| parse_dependency_entry(dep, &overrides))
721 .map(|mut dependency| {
722 let mut extra_data = dependency.extra_data.take().unwrap_or_default();
723 extra_data.insert(
724 "feature".to_string(),
725 Value::String(feature_name.to_string()),
726 );
727 dependency.extra_data = Some(extra_data);
728 dependency
729 })
730 {
731 dependencies.push(dependency);
732 }
733 }
734 }
735
736 dependencies
737}
738
739fn extract_overrides(json: &Value) -> HashMap<String, String> {
744 let Some(overrides) = json.get("overrides").and_then(Value::as_array) else {
745 return HashMap::new();
746 };
747
748 let limit = capped_iteration_limit(overrides.len(), "vcpkg overrides");
749 overrides
750 .iter()
751 .take(limit)
752 .filter_map(|entry| {
753 let name = entry
754 .get("name")
755 .and_then(Value::as_str)
756 .map(str::trim)
757 .filter(|name| !name.is_empty())?;
758 let version = manifest_version(entry)?;
759 Some((name.to_string(), truncate_field(version)))
760 })
761 .collect()
762}
763
764fn parse_dependency_entry(
765 value: &Value,
766 overrides: &HashMap<String, String>,
767) -> Option<Dependency> {
768 match value {
769 Value::String(name) => {
770 let pinned = overrides.get(name.trim());
771 Some(Dependency {
772 purl: build_vcpkg_purl(name, None).map(truncate_field),
773 extracted_requirement: Some(truncate_field(name.clone())),
774 scope: Some("dependencies".to_string()),
775 is_runtime: Some(true),
776 is_optional: Some(false),
777 is_pinned: Some(pinned.is_some()),
778 is_direct: Some(true),
779 resolved_package: None,
780 extra_data: pinned.map(|version| override_extra_data(version)),
781 })
782 }
783 Value::Object(obj) => {
784 let name = obj.get("name").and_then(Value::as_str)?.trim();
785 if name.is_empty() {
786 return None;
787 }
788
789 let extracted_requirement = obj
790 .get("version>=")
791 .and_then(Value::as_str)
792 .map(|v| truncate_field(v.to_owned()))
793 .or_else(|| Some(truncate_field(name.to_string())));
794
795 let host = obj.get("host").and_then(Value::as_bool).unwrap_or(false);
796 let pinned = overrides.get(name);
797 let mut extra = HashMap::new();
798 for field in [
799 "version>=",
800 "features",
801 "default-features",
802 "host",
803 "platform",
804 ] {
805 if let Some(field_value) = obj.get(field) {
806 extra.insert(field.to_string(), field_value.clone());
807 }
808 }
809 if let Some(version) = pinned {
810 extra.insert(
811 "override_version".to_string(),
812 Value::String(version.clone()),
813 );
814 }
815
816 Some(Dependency {
817 purl: build_vcpkg_purl(name, None).map(truncate_field),
818 extracted_requirement,
819 scope: Some("dependencies".to_string()),
820 is_runtime: Some(!host),
821 is_optional: Some(false),
822 is_pinned: Some(pinned.is_some()),
823 is_direct: Some(true),
824 resolved_package: None,
825 extra_data: (!extra.is_empty()).then_some(extra),
826 })
827 }
828 _ => None,
829 }
830}
831
832fn override_extra_data(version: &str) -> HashMap<String, Value> {
833 let mut extra = HashMap::new();
834 extra.insert(
835 "override_version".to_string(),
836 Value::String(version.to_owned()),
837 );
838 extra
839}
840
841fn build_extra_data(path: &Path, json: &Value) -> Option<HashMap<String, Value>> {
842 let mut extra = HashMap::new();
843 for field in [
844 "builtin-baseline",
845 "overrides",
846 "supports",
847 "default-features",
848 "features",
849 "configuration",
850 "vcpkg-configuration",
851 "documentation",
852 ] {
853 if let Some(value) = json.get(field) {
854 extra.insert(field.to_string(), value.clone());
855 }
856 }
857
858 if !extra.contains_key("configuration")
859 && !extra.contains_key("vcpkg-configuration")
860 && let Some(config) = read_sibling_configuration(path)
861 {
862 extra.insert("configuration".to_string(), config);
863 }
864
865 (!extra.is_empty()).then_some(extra)
866}
867
868fn read_sibling_configuration(path: &Path) -> Option<Value> {
869 let sibling_path = path.with_file_name("vcpkg-configuration.json");
870 let content = crate::parsers::utils::read_file_to_string(&sibling_path, None).ok()?;
871 match serde_json::from_str(&content) {
872 Ok(value) => Some(value),
873 Err(e) => {
874 warn!(
875 "Failed to parse sibling vcpkg-configuration.json at {:?}: {}",
876 sibling_path, e
877 );
878 None
879 }
880 }
881}
882
883fn get_non_empty_string(json: &Value, field: &str) -> Option<String> {
884 json.get(field)
885 .and_then(Value::as_str)
886 .map(str::trim)
887 .filter(|value| !value.is_empty())
888 .map(|value| value.to_string())
889}
890
891fn get_string_or_array(json: &Value, field: &str) -> Option<String> {
892 match json.get(field) {
893 Some(Value::String(s)) if !s.trim().is_empty() => Some(s.trim().to_string()),
894 Some(Value::Array(values)) => {
895 let collected: Vec<_> = values
896 .iter()
897 .filter_map(Value::as_str)
898 .map(str::trim)
899 .filter(|s| !s.is_empty())
900 .collect();
901 (!collected.is_empty()).then(|| collected.join("\n"))
902 }
903 _ => None,
904 }
905}
906
907fn build_vcpkg_purl(name: &str, version: Option<&str>) -> Option<String> {
908 let mut purl = PackageUrl::new("generic", name).ok()?;
909 purl.with_namespace("vcpkg").ok()?;
910 if let Some(version) = version {
911 purl.with_version(version).ok()?;
912 }
913 Some(purl.to_string())
914}