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