1use crate::models::{DatasourceId, Dependency, PackageData, PackageType};
22use log::warn;
23use packageurl::PackageUrl;
24use std::fs::File;
25use std::io::Read;
26use std::path::Path;
27use toml::Value;
28
29use super::PackageParser;
30
31pub struct CargoLockParser;
35
36impl PackageParser for CargoLockParser {
37 const PACKAGE_TYPE: PackageType = PackageType::Cargo;
38
39 fn is_match(path: &Path) -> bool {
40 path.file_name()
41 .and_then(|name| name.to_str())
42 .is_some_and(|name| name.eq_ignore_ascii_case("cargo.lock"))
43 }
44
45 fn extract_packages(path: &Path) -> Vec<PackageData> {
46 let content = match read_cargo_lock(path) {
47 Ok(content) => content,
48 Err(e) => {
49 warn!("Failed to read or parse Cargo.lock at {:?}: {}", path, e);
50 return vec![default_package_data()];
51 }
52 };
53
54 let packages = match content.get("package").and_then(|v| v.as_array()) {
55 Some(pkgs) => pkgs,
56 None => {
57 warn!("No 'package' array found in Cargo.lock at {:?}", path);
58 return vec![default_package_data()];
59 }
60 };
61
62 let root_package = select_root_package(packages);
63
64 let name = root_package
65 .and_then(|p| p.get("name"))
66 .and_then(|v| v.as_str())
67 .map(String::from);
68
69 let version = root_package
70 .and_then(|p| p.get("version"))
71 .and_then(|v| v.as_str())
72 .map(String::from);
73
74 let checksum = root_package
75 .and_then(|p| p.get("checksum"))
76 .and_then(|v| v.as_str())
77 .map(String::from);
78
79 let dependencies = extract_all_dependencies(packages, root_package);
80
81 let purl = match (&name, &version) {
82 (Some(n), Some(v)) => PackageUrl::new("cargo", n).ok().and_then(|mut p| {
83 p.with_version(v.as_str()).ok()?;
84 Some(p.to_string())
85 }),
86 _ => None,
87 };
88
89 let api_data_url = match (&name, &version) {
90 (Some(n), Some(v)) => Some(format!("https://crates.io/api/v1/crates/{}/{}", n, v)),
91 (Some(n), None) => Some(format!("https://crates.io/api/v1/crates/{}", n)),
92 _ => None,
93 };
94
95 vec![PackageData {
96 package_type: Some(Self::PACKAGE_TYPE),
97 namespace: None,
98 name,
99 version,
100 qualifiers: None,
101 subpath: None,
102 primary_language: None,
103 description: None,
104 release_date: None,
105 parties: Vec::new(),
106 keywords: Vec::new(),
107 homepage_url: None,
108 download_url: None,
109 size: None,
110 sha1: None,
111 md5: None,
112 sha256: checksum,
113 sha512: None,
114 bug_tracking_url: None,
115 code_view_url: None,
116 vcs_url: None,
117 copyright: None,
118 holder: None,
119 declared_license_expression: None,
120 declared_license_expression_spdx: None,
121 license_detections: Vec::new(),
122 other_license_expression: None,
123 other_license_expression_spdx: None,
124 other_license_detections: Vec::new(),
125 extracted_license_statement: None,
126 notice_text: None,
127 source_packages: Vec::new(),
128 file_references: Vec::new(),
129 is_private: false,
130 is_virtual: false,
131 extra_data: None,
132 dependencies,
133 repository_homepage_url: None,
134 repository_download_url: None,
135 api_data_url,
136 datasource_id: Some(DatasourceId::CargoLock),
137 purl,
138 }]
139 }
140}
141
142fn read_cargo_lock(path: &Path) -> Result<Value, String> {
143 let mut file = File::open(path).map_err(|e| format!("Failed to open file: {}", e))?;
144 let mut content = String::new();
145 file.read_to_string(&mut content)
146 .map_err(|e| format!("Failed to read file: {}", e))?;
147 toml::from_str(&content).map_err(|e| format!("Failed to parse TOML: {}", e))
148}
149
150fn select_root_package(packages: &[Value]) -> Option<&toml::map::Map<String, Value>> {
151 packages
152 .iter()
153 .filter_map(|package| package.as_table())
154 .find(|table| table.get("source").is_none())
155 .or_else(|| packages.first().and_then(|package| package.as_table()))
156}
157
158fn extract_all_dependencies(
159 packages: &[Value],
160 root_package: Option<&toml::map::Map<String, Value>>,
161) -> Vec<Dependency> {
162 let mut all_dependencies = Vec::new();
163
164 let package_versions: std::collections::HashMap<&str, Vec<&str>> = packages
165 .iter()
166 .filter_map(|package| package.as_table())
167 .filter_map(|table| {
168 Some((
169 table.get("name")?.as_str()?,
170 table.get("version")?.as_str()?,
171 ))
172 })
173 .fold(
174 std::collections::HashMap::new(),
175 |mut acc, (name, version)| {
176 acc.entry(name).or_default().push(version);
177 acc
178 },
179 );
180
181 let root_package_name = root_package
182 .and_then(|t| t.get("name"))
183 .and_then(|v| v.as_str())
184 .unwrap_or("");
185
186 for (index, package) in packages.iter().enumerate() {
187 if let Some(pkg_table) = package.as_table() {
188 let pkg_name = pkg_table.get("name").and_then(|v| v.as_str()).unwrap_or("");
189 let _pkg_version = pkg_table
190 .get("version")
191 .and_then(|v| v.as_str())
192 .unwrap_or("");
193
194 let is_root_package = index == 0 && pkg_name == root_package_name;
195
196 if let Some(deps) = pkg_table.get("dependencies").and_then(|v| v.as_array()) {
197 for dep in deps {
198 if let Some(dep_str) = dep.as_str() {
199 let (name, version) = parse_dependency_string(dep_str);
200 let resolved_version = if version.is_empty() {
201 package_versions
202 .get(name)
203 .and_then(|versions| (versions.len() == 1).then_some(versions[0]))
204 .unwrap_or("")
205 } else {
206 version
207 };
208
209 if !name.is_empty() {
210 let purl = if resolved_version.is_empty() {
211 PackageUrl::new("cargo", name).ok().map(|p| p.to_string())
212 } else {
213 PackageUrl::new("cargo", name).ok().and_then(|mut p| {
214 p.with_version(resolved_version).ok()?;
215 Some(p.to_string())
216 })
217 };
218
219 all_dependencies.push(Dependency {
220 purl,
221 extracted_requirement: if resolved_version.is_empty() {
222 None
223 } else {
224 Some(resolved_version.to_string())
225 },
226 scope: Some("dependencies".to_string()),
227 is_runtime: Some(true),
228 is_optional: Some(false),
229 is_pinned: Some(true),
230 is_direct: Some(is_root_package),
231 resolved_package: None,
232 extra_data: None,
233 });
234 }
235 }
236 }
237 }
238 }
239 }
240
241 all_dependencies
242}
243
244fn parse_dependency_string(dep_str: &str) -> (&str, &str) {
245 if let Some(space_idx) = dep_str.find(' ') {
246 let name = &dep_str[..space_idx];
247 let version = &dep_str[space_idx + 1..];
248 (name, version)
249 } else {
250 (dep_str, "")
251 }
252}
253
254fn default_package_data() -> PackageData {
255 PackageData {
256 package_type: Some(CargoLockParser::PACKAGE_TYPE),
257 datasource_id: Some(DatasourceId::CargoLock),
258 ..Default::default()
259 }
260}
261
262crate::register_parser!(
263 "Rust Cargo.lock lockfile",
264 &["**/Cargo.lock", "**/cargo.lock"],
265 "cargo",
266 "Rust",
267 Some("https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html"),
268);