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