provenant/parsers/
conan_data.rs1use crate::models::{DatasourceId, PackageType};
22use std::collections::HashMap;
23use std::fs;
24use std::path::Path;
25
26use log::warn;
27use serde::{Deserialize, Serialize};
28use serde_json::json;
29
30use crate::models::PackageData;
31
32use super::PackageParser;
33
34const PACKAGE_TYPE: PackageType = PackageType::Conan;
35
36fn default_package_data() -> PackageData {
37 PackageData {
38 package_type: Some(PACKAGE_TYPE),
39 primary_language: Some("C++".to_string()),
40 datasource_id: Some(DatasourceId::ConanConanDataYml),
41 ..Default::default()
42 }
43}
44
45pub struct ConanDataParser;
47
48#[derive(Debug, Deserialize, Serialize)]
49struct ConanDataYml {
50 sources: Option<HashMap<String, SourceInfo>>,
51 patches: Option<HashMap<String, PatchesValue>>,
52}
53
54#[derive(Debug, Deserialize, Serialize)]
55#[serde(untagged)]
56enum UrlValue {
57 Single(String),
58 Multiple(Vec<String>),
59}
60
61#[derive(Debug, Deserialize, Serialize)]
62#[serde(untagged)]
63enum PatchesValue {
64 List(Vec<PatchInfo>),
65 String(String),
66}
67
68#[derive(Debug, Deserialize, Serialize)]
69struct PatchInfo {
70 patch_file: Option<String>,
71 patch_description: Option<String>,
72 patch_type: Option<String>,
73}
74
75#[derive(Debug, Deserialize, Serialize)]
76struct SourceInfo {
77 url: Option<UrlValue>,
78 sha256: Option<String>,
79}
80
81impl PackageParser for ConanDataParser {
82 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
83
84 fn is_match(path: &Path) -> bool {
85 path.to_str().is_some_and(|p| p.ends_with("/conandata.yml"))
86 }
87
88 fn extract_packages(path: &Path) -> Vec<PackageData> {
89 let content = match fs::read_to_string(path) {
90 Ok(c) => c,
91 Err(e) => {
92 warn!("Failed to read conandata.yml file {:?}: {}", path, e);
93 return vec![default_package_data()];
94 }
95 };
96
97 parse_conandata_yml(&content)
98 }
99}
100
101pub(crate) fn parse_conandata_yml(content: &str) -> Vec<PackageData> {
102 let data: ConanDataYml = match serde_yaml::from_str(content) {
103 Ok(d) => d,
104 Err(e) => {
105 warn!("Failed to parse conandata.yml: {}", e);
106 return vec![default_package_data()];
107 }
108 };
109
110 let Some(sources) = data.sources else {
111 return vec![default_package_data()];
112 };
113
114 let mut packages = Vec::new();
115
116 for (version, source_info) in sources {
117 let mut extra_data = HashMap::new();
118
119 let download_url = match &source_info.url {
120 Some(UrlValue::Single(url)) => Some(url.clone()),
121 Some(UrlValue::Multiple(urls)) if !urls.is_empty() => Some(urls[0].clone()),
122 _ => None,
123 };
124
125 if let Some(UrlValue::Multiple(urls)) = &source_info.url
126 && urls.len() > 1
127 {
128 extra_data.insert("mirror_urls".to_string(), json!(urls));
129 }
130
131 if let Some(ref patches_map) = data.patches
132 && let Some(patches_value) = patches_map.get(&version)
133 {
134 let patches_json = match patches_value {
135 PatchesValue::List(patches) => {
136 let patches_data: Vec<_> = patches
137 .iter()
138 .map(|p| {
139 json!({
140 "patch_file": p.patch_file,
141 "patch_description": p.patch_description,
142 "patch_type": p.patch_type,
143 })
144 })
145 .collect();
146 json!(patches_data)
147 }
148 PatchesValue::String(s) => json!(s),
149 };
150 extra_data.insert("patches".to_string(), patches_json);
151 }
152
153 packages.push(PackageData {
154 package_type: Some(PACKAGE_TYPE),
155 primary_language: Some("C++".to_string()),
156 version: Some(version),
157 download_url,
158 sha256: source_info.sha256,
159 extra_data: if extra_data.is_empty() {
160 None
161 } else {
162 Some(extra_data)
163 },
164 datasource_id: Some(DatasourceId::ConanConanDataYml),
165 ..Default::default()
166 });
167 }
168
169 if packages.is_empty() {
170 packages.push(default_package_data());
171 }
172
173 packages
174}
175
176crate::register_parser!(
177 "Conan external source metadata",
178 &["*/conandata.yml"],
179 "conan",
180 "C++",
181 Some("https://docs.conan.io/2/tutorial/creating_packages/handle_sources_in_packages.html"),
182);