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);