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, Sha256Digest};
22use std::collections::HashMap;
23use std::path::Path;
24
25use crate::parser_warn as warn;
26use serde::{Deserialize, Serialize};
27use serde_json::json;
28
29use crate::models::PackageData;
30use crate::parsers::utils::{MAX_ITERATION_COUNT, read_file_to_string, truncate_field};
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 read_file_to_string(path, None) {
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 yaml_serde::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.into_iter().take(MAX_ITERATION_COUNT) {
117        let mut extra_data = HashMap::new();
118
119        let download_url = match &source_info.url {
120            Some(UrlValue::Single(url)) => Some(truncate_field(url.clone())),
121            Some(UrlValue::Multiple(urls)) if !urls.is_empty() => {
122                Some(truncate_field(urls[0].clone()))
123            }
124            _ => None,
125        };
126
127        if let Some(UrlValue::Multiple(urls)) = &source_info.url
128            && urls.len() > 1
129        {
130            extra_data.insert("mirror_urls".to_string(), json!(urls));
131        }
132
133        if let Some(ref patches_map) = data.patches
134            && let Some(patches_value) = patches_map.get(&version)
135        {
136            let patches_json = match patches_value {
137                PatchesValue::List(patches) => {
138                    let patches_data: Vec<_> = patches
139                        .iter()
140                        .map(|p| {
141                            json!({
142                                "patch_file": p.patch_file,
143                                "patch_description": p.patch_description,
144                                "patch_type": p.patch_type,
145                            })
146                        })
147                        .collect();
148                    json!(patches_data)
149                }
150                PatchesValue::String(s) => json!(s),
151            };
152            extra_data.insert("patches".to_string(), patches_json);
153        }
154
155        packages.push(PackageData {
156            package_type: Some(PACKAGE_TYPE),
157            primary_language: Some("C++".to_string()),
158            version: Some(truncate_field(version)),
159            download_url,
160            sha256: source_info
161                .sha256
162                .and_then(|h| Sha256Digest::from_hex(&h).ok()),
163            extra_data: if extra_data.is_empty() {
164                None
165            } else {
166                Some(extra_data)
167            },
168            datasource_id: Some(DatasourceId::ConanConanDataYml),
169            ..Default::default()
170        });
171    }
172
173    if packages.is_empty() {
174        packages.push(default_package_data());
175    }
176
177    packages
178}
179
180crate::register_parser!(
181    "Conan external source metadata",
182    &["*/conandata.yml"],
183    "conan",
184    "C++",
185    Some("https://docs.conan.io/2/tutorial/creating_packages/handle_sources_in_packages.html"),
186);