Skip to main content

provenant/parsers/
vcpkg.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4use 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 = &paragraphs[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(&paragraph.headers, "version")?;
323    let port_version = rfc822::get_header_first(&paragraph.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(&paragraph.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(&paragraph.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(&paragraph.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(&paragraph.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
559/// Preserve registry and overlay provenance from a standalone
560/// `vcpkg-configuration.json`. This file declares where dependencies are
561/// resolved from (registries) and which local overlays apply, but it has no
562/// package identity of its own, so the metadata is preserved verbatim.
563fn 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
739/// Map of dependency name to the exact version pinned by the manifest's
740/// `overrides` array. An override is an author-declared hard pin, so it proves
741/// version intent even though the dependency entry itself only declares a
742/// `version>=` floor.
743fn 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}