Skip to main content

package_parser/pkgs/
conan.rs

1use std::{collections::HashMap, fs::File, path::Path};
2
3use packageurl::PackageUrl;
4use crate::types::{DependentPackage, Package};
5use serde::Deserialize;
6
7use crate::{error::SourcePkgError, PackageManifest};
8
9/// Dependency graph node for version 0.4.
10#[derive(Debug, Deserialize)]
11struct GraphNodeV04 {
12    #[serde(rename = "ref")]
13    ref_: Option<String>,
14    path: Option<String>,
15    #[serde(default)]
16    requires: Vec<String>,
17    prev: Option<String>,
18}
19
20/// Dependency graph for version 0.4.
21#[derive(Debug, Deserialize)]
22struct GraphLockV04 {
23    nodes: HashMap<String, GraphNodeV04>,
24}
25
26/// Parse a dependency string for version 0.5.
27///
28/// `pkg/1.1#revision%timestamp`
29fn parse_deps_v05(dep: &str) -> Option<DependentPackage> {
30    let (name, dep) = dep.split_once('/')?;
31    let (version, dep) = dep.split_once('#')?;
32    let (hash, timestamp) = dep.split_once('%')?;
33
34    let mut purl = PackageUrl::new("conan", name).unwrap();
35    purl.with_version(version);
36    purl.add_qualifier("rev", hash).ok();
37    purl.add_qualifier("timestamp", timestamp).ok();
38
39    Some(DependentPackage {
40        purl: purl.to_string(),
41        requirement: version.into(),
42        is_resolved: true,
43        ..Default::default()
44    })
45}
46
47#[derive(Debug, Deserialize)]
48#[serde(tag = "version")]
49enum LockFile {
50    #[serde(rename = "0.5")]
51    V05 {
52        #[serde(default)]
53        requires: Vec<String>,
54        #[serde(default)]
55        build_requires: Vec<String>,
56        // #[serde(default)]
57        // python_requires: Vec<String>,
58    },
59    #[serde(rename = "0.4")]
60    V04 { graph_lock: GraphLockV04 },
61}
62
63pub struct ConanLock {}
64
65impl ConanLock {
66    pub fn new() -> Self {
67        Self {}
68    }
69
70    fn parse(path: &Path) -> Result<Package, SourcePkgError> {
71        let file = File::open(path)?;
72        let lock_file: LockFile = serde_json::from_reader(file)?;
73
74        let mut deps = vec![];
75
76        match lock_file {
77            LockFile::V05 {
78                requires,
79                build_requires,
80                // python_requires: _,
81            } => {
82                log::info!("Got Conan v0.5 lock file.");
83
84                for req in requires {
85                    let dep = if let Some(dep) = parse_deps_v05(&req) {
86                        dep
87                    } else {
88                        log::warn!("Failed to parse dependency: {}", req);
89                        continue;
90                    };
91                    deps.push(dep);
92                }
93
94                for req in build_requires {
95                    let mut dep = if let Some(dep) = parse_deps_v05(&req) {
96                        dep
97                    } else {
98                        continue;
99                    };
100                    dep.scope = "build".to_string();
101                    dep.is_runtime = false;
102                    deps.push(dep);
103                }
104            }
105            LockFile::V04 { graph_lock } => {
106                log::info!("Got Conan v0.4 lock file.");
107
108                let mut nodes = HashMap::new();
109
110                for (id, node) in &graph_lock.nodes {
111                    if let Some(path) = &node.path {
112                        if path.starts_with("conanfile") {
113                            // This is the root node.
114                            continue;
115                        }
116                    }
117
118                    let ref_ = if let Some(ref_) = &node.ref_ {
119                        ref_
120                    } else {
121                        continue;
122                    };
123
124                    let (name, version) = if let Some((n, v)) = ref_.split_once('/') {
125                        (n, v)
126                    } else {
127                        log::warn!("Failed to parse dependency: {}", ref_);
128                        continue;
129                    };
130
131                    let mut purl = PackageUrl::new("conan", name).unwrap();
132                    purl.with_version(version);
133                    if let Some(prev) = &node.prev {
134                        purl.add_qualifier("prev", prev).ok();
135                    }
136
137                    nodes.insert(
138                        id.clone(),
139                        DependentPackage {
140                            purl: purl.to_string(),
141                            requirement: version.into(),
142                            is_resolved: true,
143                            ..Default::default()
144                        },
145                    );
146                }
147
148                for (id, node) in &graph_lock.nodes {
149                    if let Some(path) = &node.path {
150                        if path.starts_with("conanfile") {
151                            // This is the root node.
152
153                            for req in &node.requires {
154                                if let Some(dep_node) = nodes.get_mut(req) {
155                                    dep_node.relation.insert(crate::types::Relation::Direct);
156                                }
157                            }
158
159                            continue;
160                        }
161                    }
162
163                    let current_purl = if let Some(current_node) = nodes.get(id) {
164                        current_node.purl.clone()
165                    } else {
166                        continue;
167                    };
168
169                    for req in &node.requires {
170                        if let Some(dep_node) = nodes.get_mut(req) {
171                            dep_node.parents.insert(current_purl.clone());
172                            dep_node
173                                .relation
174                                .insert(crate::types::Relation::Indirect);
175                        }
176                    }
177                }
178
179                deps.extend(nodes.into_values());
180            }
181        }
182
183        Ok(Package {
184            dependencies: deps,
185            ..Default::default()
186        })
187    }
188}
189
190#[async_trait::async_trait]
191impl PackageManifest for ConanLock {
192    fn get_name(&self) -> String {
193        "conan".to_string()
194    }
195
196    async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
197        Self::parse(path)
198    }
199
200    fn file_name_patterns(&self) -> &'static [&'static str] {
201        &["conan.lock"]
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn v05() {
211        let filepath = Path::new(concat!(
212            env!("CARGO_MANIFEST_DIR"),
213            "/testdata/conan/v05.conan.lock"
214        ));
215
216        let package = ConanLock::parse(filepath).unwrap();
217        println!("{:?}", package);
218    }
219
220    #[test]
221    fn v04() {
222        let filepath = Path::new(concat!(
223            env!("CARGO_MANIFEST_DIR"),
224            "/testdata/conan/v04.conan.lock"
225        ));
226
227        let package = ConanLock::parse(filepath).unwrap();
228        println!("{:?}", package);
229    }
230}