Skip to main content

provenant/parsers/
vcpkg.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::Path;
4
5use crate::parser_warn as warn;
6use packageurl::PackageUrl;
7use serde_json::Value;
8
9use crate::models::{DatasourceId, Dependency, PackageData, PackageType, Party};
10use crate::parsers::utils::split_name_email;
11
12use super::PackageParser;
13
14pub struct VcpkgManifestParser;
15
16impl PackageParser for VcpkgManifestParser {
17    const PACKAGE_TYPE: PackageType = PackageType::Vcpkg;
18
19    fn is_match(path: &Path) -> bool {
20        path.file_name().and_then(|name| name.to_str()) == Some("vcpkg.json")
21    }
22
23    fn extract_packages(path: &Path) -> Vec<PackageData> {
24        let content = match fs::read_to_string(path) {
25            Ok(content) => content,
26            Err(e) => {
27                warn!("Failed to read vcpkg.json at {:?}: {}", path, e);
28                return vec![default_package_data()];
29            }
30        };
31
32        let json: Value = match serde_json::from_str(&content) {
33            Ok(json) => json,
34            Err(e) => {
35                warn!("Failed to parse vcpkg.json at {:?}: {}", path, e);
36                return vec![default_package_data()];
37            }
38        };
39
40        vec![parse_vcpkg_manifest(path, &json)]
41    }
42}
43
44fn default_package_data() -> PackageData {
45    PackageData {
46        package_type: Some(PackageType::Vcpkg),
47        datasource_id: Some(DatasourceId::VcpkgJson),
48        ..Default::default()
49    }
50}
51
52fn parse_vcpkg_manifest(path: &Path, json: &Value) -> PackageData {
53    let name = get_non_empty_string(json, "name");
54    let version = manifest_version(json);
55    let description = get_string_or_array(json, "description");
56    let homepage_url = get_non_empty_string(json, "homepage");
57    let extracted_license_statement = get_string_or_array(json, "license");
58    let parties = extract_maintainers(json);
59    let dependencies = extract_dependencies(json);
60    let extra_data = build_extra_data(path, json);
61
62    PackageData {
63        package_type: Some(PackageType::Vcpkg),
64        namespace: None,
65        name: name.clone(),
66        version: version.clone(),
67        primary_language: Some("C++".to_string()),
68        description,
69        parties,
70        homepage_url,
71        extracted_license_statement,
72        is_private: name.is_none(),
73        dependencies,
74        extra_data,
75        datasource_id: Some(DatasourceId::VcpkgJson),
76        purl: name
77            .as_deref()
78            .and_then(|name| build_vcpkg_purl(name, version.as_deref())),
79        ..default_package_data()
80    }
81}
82
83fn manifest_version(json: &Value) -> Option<String> {
84    let version = [
85        "version",
86        "version-semver",
87        "version-date",
88        "version-string",
89    ]
90    .into_iter()
91    .find_map(|field| get_non_empty_string(json, field));
92
93    match (version, json.get("port-version").and_then(Value::as_i64)) {
94        (Some(version), Some(port_version)) if port_version > 0 => {
95            Some(format!("{}#{}", version, port_version))
96        }
97        (version, _) => version,
98    }
99}
100
101fn extract_maintainers(json: &Value) -> Vec<Party> {
102    let Some(value) = json.get("maintainers") else {
103        return Vec::new();
104    };
105
106    let maintainers: Vec<String> = match value {
107        Value::String(s) => vec![s.clone()],
108        Value::Array(values) => values
109            .iter()
110            .filter_map(Value::as_str)
111            .map(ToOwned::to_owned)
112            .collect(),
113        _ => Vec::new(),
114    };
115
116    maintainers
117        .into_iter()
118        .map(|entry| {
119            let (name, email) = split_name_email(&entry);
120            Party {
121                r#type: Some("person".to_string()),
122                role: Some("maintainer".to_string()),
123                name,
124                email,
125                url: None,
126                organization: None,
127                organization_url: None,
128                timezone: None,
129            }
130        })
131        .collect()
132}
133
134fn extract_dependencies(json: &Value) -> Vec<Dependency> {
135    let mut dependencies: Vec<Dependency> = json
136        .get("dependencies")
137        .and_then(Value::as_array)
138        .map(|deps| deps.iter().filter_map(parse_dependency_entry).collect())
139        .unwrap_or_default();
140
141    if let Some(features) = json.get("features").and_then(Value::as_object) {
142        for (feature_name, feature_value) in features {
143            let Some(feature_dependencies) =
144                feature_value.get("dependencies").and_then(Value::as_array)
145            else {
146                continue;
147            };
148
149            for dependency in feature_dependencies
150                .iter()
151                .filter_map(parse_dependency_entry)
152                .map(|mut dependency| {
153                    let mut extra_data = dependency.extra_data.take().unwrap_or_default();
154                    extra_data.insert(
155                        "feature".to_string(),
156                        Value::String(feature_name.to_string()),
157                    );
158                    dependency.extra_data = Some(extra_data);
159                    dependency
160                })
161            {
162                dependencies.push(dependency);
163            }
164        }
165    }
166
167    dependencies
168}
169
170fn parse_dependency_entry(value: &Value) -> Option<Dependency> {
171    match value {
172        Value::String(name) => Some(Dependency {
173            purl: build_vcpkg_purl(name, None),
174            extracted_requirement: Some(name.clone()),
175            scope: Some("dependencies".to_string()),
176            is_runtime: Some(true),
177            is_optional: Some(false),
178            is_pinned: Some(false),
179            is_direct: Some(true),
180            resolved_package: None,
181            extra_data: None,
182        }),
183        Value::Object(obj) => {
184            let name = obj.get("name").and_then(Value::as_str)?.trim();
185            if name.is_empty() {
186                return None;
187            }
188
189            let extracted_requirement = obj
190                .get("version>=")
191                .and_then(Value::as_str)
192                .map(ToOwned::to_owned)
193                .or_else(|| Some(name.to_string()));
194
195            let host = obj.get("host").and_then(Value::as_bool).unwrap_or(false);
196            let mut extra = HashMap::new();
197            for field in [
198                "version>=",
199                "features",
200                "default-features",
201                "host",
202                "platform",
203            ] {
204                if let Some(field_value) = obj.get(field) {
205                    extra.insert(field.to_string(), field_value.clone());
206                }
207            }
208
209            Some(Dependency {
210                purl: build_vcpkg_purl(name, None),
211                extracted_requirement,
212                scope: Some("dependencies".to_string()),
213                is_runtime: Some(!host),
214                is_optional: Some(false),
215                is_pinned: Some(false),
216                is_direct: Some(true),
217                resolved_package: None,
218                extra_data: (!extra.is_empty()).then_some(extra),
219            })
220        }
221        _ => None,
222    }
223}
224
225fn build_extra_data(path: &Path, json: &Value) -> Option<HashMap<String, Value>> {
226    let mut extra = HashMap::new();
227    for field in [
228        "builtin-baseline",
229        "overrides",
230        "supports",
231        "default-features",
232        "features",
233        "configuration",
234        "vcpkg-configuration",
235        "documentation",
236    ] {
237        if let Some(value) = json.get(field) {
238            extra.insert(field.to_string(), value.clone());
239        }
240    }
241
242    if !extra.contains_key("configuration")
243        && !extra.contains_key("vcpkg-configuration")
244        && let Some(config) = read_sibling_configuration(path)
245    {
246        extra.insert("configuration".to_string(), config);
247    }
248
249    (!extra.is_empty()).then_some(extra)
250}
251
252fn read_sibling_configuration(path: &Path) -> Option<Value> {
253    let sibling_path = path.with_file_name("vcpkg-configuration.json");
254    let content = fs::read_to_string(&sibling_path).ok()?;
255    match serde_json::from_str(&content) {
256        Ok(value) => Some(value),
257        Err(e) => {
258            warn!(
259                "Failed to parse sibling vcpkg-configuration.json at {:?}: {}",
260                sibling_path, e
261            );
262            None
263        }
264    }
265}
266
267fn get_non_empty_string(json: &Value, field: &str) -> Option<String> {
268    json.get(field)
269        .and_then(Value::as_str)
270        .map(str::trim)
271        .filter(|value| !value.is_empty())
272        .map(|value| value.to_string())
273}
274
275fn get_string_or_array(json: &Value, field: &str) -> Option<String> {
276    match json.get(field) {
277        Some(Value::String(s)) if !s.trim().is_empty() => Some(s.trim().to_string()),
278        Some(Value::Array(values)) => {
279            let collected: Vec<_> = values
280                .iter()
281                .filter_map(Value::as_str)
282                .map(str::trim)
283                .filter(|s| !s.is_empty())
284                .collect();
285            (!collected.is_empty()).then(|| collected.join("\n"))
286        }
287        _ => None,
288    }
289}
290
291fn build_vcpkg_purl(name: &str, version: Option<&str>) -> Option<String> {
292    let mut purl = PackageUrl::new("generic", name).ok()?;
293    purl.with_namespace("vcpkg").ok()?;
294    if let Some(version) = version {
295        purl.with_version(version).ok()?;
296    }
297    Some(purl.to_string())
298}
299
300crate::register_parser!(
301    "vcpkg manifest file",
302    &["**/vcpkg.json"],
303    "vcpkg",
304    "",
305    Some("https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-json"),
306);