Skip to main content

provenant/parsers/
conan_data.rs

1//! Parser for Conan conandata.yml files.
2//!
3//! Extracts package metadata from `conandata.yml` files which contain
4//! external source information for Conan packages.
5//!
6//! # Supported Formats
7//! - `conandata.yml` - Conan external source metadata
8//!
9//! # Key Features
10//! - Version-specific source URLs
11//! - SHA256 checksums
12//! - Multiple source mirrors support
13//! - Patch metadata extraction (beyond Python which ignores patches)
14//!
15//! # Implementation Notes
16//! - Format: YAML with `sources` dict containing version→{url, sha256}
17//! - Each version can have multiple URLs (list or single string)
18//! - Patches section contains version→[{patch_file, patch_description, patch_type}]
19//! - Spec: https://docs.conan.io/2/tutorial/creating_packages/handle_sources_in_packages.html
20
21use 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
45/// Parser for Conan conandata.yml files
46pub 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);