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 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) => values
122 .iter()
123 .take(MAX_ITERATION_COUNT)
124 .filter_map(Value::as_str)
125 .map(ToOwned::to_owned)
126 .collect(),
127 _ => Vec::new(),
128 };
129
130 maintainers
131 .into_iter()
132 .map(|entry| {
133 let (name, email) = split_name_email(&entry);
134 Party {
135 r#type: Some("person".to_string()),
136 role: Some("maintainer".to_string()),
137 name: name.map(truncate_field),
138 email: email.map(truncate_field),
139 url: None,
140 organization: None,
141 organization_url: None,
142 timezone: None,
143 }
144 })
145 .collect()
146}
147
148fn extract_dependencies(json: &Value) -> Vec<Dependency> {
149 let mut dependencies: Vec<Dependency> = json
150 .get("dependencies")
151 .and_then(Value::as_array)
152 .map(|deps| {
153 deps.iter()
154 .take(MAX_ITERATION_COUNT)
155 .filter_map(parse_dependency_entry)
156 .collect()
157 })
158 .unwrap_or_default();
159
160 if let Some(features) = json.get("features").and_then(Value::as_object) {
161 for (feature_name, feature_value) in features.iter().take(MAX_ITERATION_COUNT) {
162 let Some(feature_dependencies) =
163 feature_value.get("dependencies").and_then(Value::as_array)
164 else {
165 continue;
166 };
167
168 for dependency in feature_dependencies
169 .iter()
170 .take(MAX_ITERATION_COUNT)
171 .filter_map(parse_dependency_entry)
172 .map(|mut dependency| {
173 let mut extra_data = dependency.extra_data.take().unwrap_or_default();
174 extra_data.insert(
175 "feature".to_string(),
176 Value::String(feature_name.to_string()),
177 );
178 dependency.extra_data = Some(extra_data);
179 dependency
180 })
181 {
182 dependencies.push(dependency);
183 }
184 }
185 }
186
187 dependencies
188}
189
190fn parse_dependency_entry(value: &Value) -> Option<Dependency> {
191 match value {
192 Value::String(name) => Some(Dependency {
193 purl: build_vcpkg_purl(name, None).map(truncate_field),
194 extracted_requirement: Some(truncate_field(name.clone())),
195 scope: Some("dependencies".to_string()),
196 is_runtime: Some(true),
197 is_optional: Some(false),
198 is_pinned: Some(false),
199 is_direct: Some(true),
200 resolved_package: None,
201 extra_data: None,
202 }),
203 Value::Object(obj) => {
204 let name = obj.get("name").and_then(Value::as_str)?.trim();
205 if name.is_empty() {
206 return None;
207 }
208
209 let extracted_requirement = obj
210 .get("version>=")
211 .and_then(Value::as_str)
212 .map(|v| truncate_field(v.to_owned()))
213 .or_else(|| Some(truncate_field(name.to_string())));
214
215 let host = obj.get("host").and_then(Value::as_bool).unwrap_or(false);
216 let mut extra = HashMap::new();
217 for field in [
218 "version>=",
219 "features",
220 "default-features",
221 "host",
222 "platform",
223 ] {
224 if let Some(field_value) = obj.get(field) {
225 extra.insert(field.to_string(), field_value.clone());
226 }
227 }
228
229 Some(Dependency {
230 purl: build_vcpkg_purl(name, None).map(truncate_field),
231 extracted_requirement,
232 scope: Some("dependencies".to_string()),
233 is_runtime: Some(!host),
234 is_optional: Some(false),
235 is_pinned: Some(false),
236 is_direct: Some(true),
237 resolved_package: None,
238 extra_data: (!extra.is_empty()).then_some(extra),
239 })
240 }
241 _ => None,
242 }
243}
244
245fn build_extra_data(path: &Path, json: &Value) -> Option<HashMap<String, Value>> {
246 let mut extra = HashMap::new();
247 for field in [
248 "builtin-baseline",
249 "overrides",
250 "supports",
251 "default-features",
252 "features",
253 "configuration",
254 "vcpkg-configuration",
255 "documentation",
256 ] {
257 if let Some(value) = json.get(field) {
258 extra.insert(field.to_string(), value.clone());
259 }
260 }
261
262 if !extra.contains_key("configuration")
263 && !extra.contains_key("vcpkg-configuration")
264 && let Some(config) = read_sibling_configuration(path)
265 {
266 extra.insert("configuration".to_string(), config);
267 }
268
269 (!extra.is_empty()).then_some(extra)
270}
271
272fn read_sibling_configuration(path: &Path) -> Option<Value> {
273 let sibling_path = path.with_file_name("vcpkg-configuration.json");
274 let content = crate::parsers::utils::read_file_to_string(&sibling_path, None).ok()?;
275 match serde_json::from_str(&content) {
276 Ok(value) => Some(value),
277 Err(e) => {
278 warn!(
279 "Failed to parse sibling vcpkg-configuration.json at {:?}: {}",
280 sibling_path, e
281 );
282 None
283 }
284 }
285}
286
287fn get_non_empty_string(json: &Value, field: &str) -> Option<String> {
288 json.get(field)
289 .and_then(Value::as_str)
290 .map(str::trim)
291 .filter(|value| !value.is_empty())
292 .map(|value| value.to_string())
293}
294
295fn get_string_or_array(json: &Value, field: &str) -> Option<String> {
296 match json.get(field) {
297 Some(Value::String(s)) if !s.trim().is_empty() => Some(s.trim().to_string()),
298 Some(Value::Array(values)) => {
299 let collected: Vec<_> = values
300 .iter()
301 .filter_map(Value::as_str)
302 .map(str::trim)
303 .filter(|s| !s.is_empty())
304 .collect();
305 (!collected.is_empty()).then(|| collected.join("\n"))
306 }
307 _ => None,
308 }
309}
310
311fn build_vcpkg_purl(name: &str, version: Option<&str>) -> Option<String> {
312 let mut purl = PackageUrl::new("generic", name).ok()?;
313 purl.with_namespace("vcpkg").ok()?;
314 if let Some(version) = version {
315 purl.with_version(version).ok()?;
316 }
317 Some(purl.to_string())
318}