provenant/parsers/
conan_data.rs1use crate::models::{DatasourceId, PackageType, Sha256Digest};
27use std::collections::HashMap;
28use std::path::Path;
29
30use crate::parser_warn as warn;
31use serde::{Deserialize, Serialize};
32use serde_json::json;
33
34use crate::models::PackageData;
35use crate::parsers::utils::{capped_iteration_limit, read_file_to_string, truncate_field};
36
37use super::PackageParser;
38
39const PACKAGE_TYPE: PackageType = PackageType::Conan;
40
41fn default_package_data() -> PackageData {
42 PackageData {
43 package_type: Some(PACKAGE_TYPE),
44 primary_language: Some("C++".to_string()),
45 datasource_id: Some(DatasourceId::ConanConanDataYml),
46 ..Default::default()
47 }
48}
49
50pub struct ConanDataParser;
52
53#[derive(Debug, Deserialize, Serialize)]
54struct ConanDataYml {
55 sources: Option<HashMap<String, SourcesValue>>,
56 patches: Option<HashMap<String, PatchesValue>>,
57}
58
59#[derive(Debug, Deserialize, Serialize)]
60#[serde(untagged)]
61enum SourcesValue {
62 Single(SourceInfo),
63 Multiple(Vec<SourceInfo>),
64}
65
66#[derive(Debug, Deserialize, Serialize)]
67#[serde(untagged)]
68enum UrlValue {
69 Single(String),
70 Multiple(Vec<String>),
71}
72
73#[derive(Debug, Deserialize, Serialize)]
74#[serde(untagged)]
75enum PatchesValue {
76 List(Vec<PatchInfo>),
77 String(String),
78}
79
80#[derive(Debug, Deserialize, Serialize)]
81struct PatchInfo {
82 patch_file: Option<String>,
83 patch_description: Option<String>,
84 patch_type: Option<String>,
85}
86
87#[derive(Debug, Deserialize, Serialize)]
88struct SourceInfo {
89 url: Option<UrlValue>,
90 sha256: Option<String>,
91}
92
93impl SourceInfo {
94 fn primary_download_url(&self) -> Option<String> {
95 match &self.url {
96 Some(UrlValue::Single(url)) => Some(truncate_field(url.clone())),
97 Some(UrlValue::Multiple(urls)) if !urls.is_empty() => {
98 Some(truncate_field(urls[0].clone()))
99 }
100 _ => None,
101 }
102 }
103
104 fn additional_data_json(&self) -> serde_json::Value {
105 let mut entry = serde_json::Map::new();
106
107 if let Some(url) = &self.url {
108 match url {
109 UrlValue::Single(value) => {
110 entry.insert("url".to_string(), json!(truncate_field(value.clone())));
111 }
112 UrlValue::Multiple(values) => {
113 let urls: Vec<_> = values.iter().cloned().map(truncate_field).collect();
114 entry.insert("url".to_string(), json!(urls));
115 }
116 }
117 }
118
119 if let Some(sha256) = &self.sha256 {
120 entry.insert("sha256".to_string(), json!(sha256));
121 }
122
123 serde_json::Value::Object(entry)
124 }
125}
126
127fn sources_to_infos(sources_value: SourcesValue) -> Vec<SourceInfo> {
128 match sources_value {
129 SourcesValue::Single(source) => vec![source],
130 SourcesValue::Multiple(sources) => sources,
131 }
132}
133
134impl PackageParser for ConanDataParser {
135 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
136
137 fn is_match(path: &Path) -> bool {
138 path.to_str().is_some_and(|p| p.ends_with("/conandata.yml"))
139 }
140
141 fn extract_packages(path: &Path) -> Vec<PackageData> {
142 let content = match read_file_to_string(path, None) {
143 Ok(c) => c,
144 Err(e) => {
145 warn!("Failed to read conandata.yml file {:?}: {}", path, e);
146 return vec![default_package_data()];
147 }
148 };
149
150 parse_conandata_yml(&content)
151 }
152
153 fn metadata() -> Vec<super::metadata::ParserMetadata> {
154 vec![super::metadata::ParserMetadata {
155 description: "Conan external source metadata",
156 file_patterns: &["*/conandata.yml"],
157 package_type: "conan",
158 primary_language: "C++",
159 documentation_url: Some(
160 "https://docs.conan.io/2/tutorial/creating_packages/handle_sources_in_packages.html",
161 ),
162 }]
163 }
164}
165
166pub(crate) fn parse_conandata_yml(content: &str) -> Vec<PackageData> {
167 let data: ConanDataYml = match yaml_serde::from_str(content) {
168 Ok(d) => d,
169 Err(e) => {
170 warn!("Failed to parse conandata.yml: {}", e);
171 return vec![default_package_data()];
172 }
173 };
174
175 let Some(sources) = data.sources else {
176 return vec![default_package_data()];
177 };
178
179 let mut packages = Vec::new();
180
181 let limit = capped_iteration_limit(sources.len(), "conandata.yml sources");
184 let mut sources: Vec<_> = sources.into_iter().collect();
185 sources.sort_by(|(a, _), (b, _)| a.cmp(b));
186
187 for (version, sources_value) in sources.into_iter().take(limit) {
188 let source_infos = sources_to_infos(sources_value);
189 let mut extra_data = HashMap::new();
190
191 let primary_index = source_infos
192 .iter()
193 .position(|source_info| {
194 source_info.url.is_some()
195 || source_info
196 .sha256
197 .as_ref()
198 .is_some_and(|value| !value.is_empty())
199 })
200 .unwrap_or(0);
201
202 let primary_source = source_infos.get(primary_index);
203
204 let download_url = primary_source.and_then(SourceInfo::primary_download_url);
205
206 if let Some(UrlValue::Multiple(urls)) =
207 primary_source.and_then(|source| source.url.as_ref())
208 && urls.len() > 1
209 {
210 let mirror_urls: Vec<_> = urls.iter().cloned().map(truncate_field).collect();
211 extra_data.insert("mirror_urls".to_string(), json!(mirror_urls));
212 }
213
214 if source_infos.len() > 1 {
215 let additional_sources: Vec<_> = source_infos
216 .iter()
217 .enumerate()
218 .filter(|(index, _)| *index != primary_index)
219 .map(|(_, source_info)| source_info.additional_data_json())
220 .collect();
221
222 if !additional_sources.is_empty() {
223 extra_data.insert("additional_sources".to_string(), json!(additional_sources));
224 }
225 }
226
227 if let Some(ref patches_map) = data.patches
228 && let Some(patches_value) = patches_map.get(&version)
229 {
230 let patches_json = match patches_value {
231 PatchesValue::List(patches) => {
232 let patches_data: Vec<_> = patches
233 .iter()
234 .map(|p| {
235 json!({
236 "patch_file": p.patch_file,
237 "patch_description": p.patch_description,
238 "patch_type": p.patch_type,
239 })
240 })
241 .collect();
242 json!(patches_data)
243 }
244 PatchesValue::String(s) => json!(s),
245 };
246 extra_data.insert("patches".to_string(), patches_json);
247 }
248
249 packages.push(PackageData {
250 package_type: Some(PACKAGE_TYPE),
251 primary_language: Some("C++".to_string()),
252 version: Some(truncate_field(version)),
253 download_url,
254 sha256: primary_source
255 .and_then(|source_info| source_info.sha256.as_deref())
256 .and_then(|hash| Sha256Digest::from_hex(hash).ok()),
257 extra_data: if extra_data.is_empty() {
258 None
259 } else {
260 Some(extra_data)
261 },
262 datasource_id: Some(DatasourceId::ConanConanDataYml),
263 ..Default::default()
264 });
265 }
266
267 if packages.is_empty() {
268 packages.push(default_package_data());
269 }
270
271 packages
272}