package_parser/pkgs/
conan.rs1use 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#[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#[derive(Debug, Deserialize)]
22struct GraphLockV04 {
23 nodes: HashMap<String, GraphNodeV04>,
24}
25
26fn 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 },
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 } => {
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 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 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}