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