package_parser/pkgs/
cargo.rs1use cargo_lock::Lockfile as CargoLockfile;
2use cargo_manifest::Dependency;
3use cargo_manifest::Error as CargoManifestError;
4use cargo_manifest::Manifest as CargoManifest;
5use maplit::hashset;
6use packageurl::PackageUrl;
7use crate::types::Relation;
8use std::{path::Path, str::FromStr};
9use toml::value::Value;
10
11use crate::error::SourcePkgError;
12use crate::pkgs::common::model::{Package, PackageManifest};
13
14use super::common::model::DependentPackage;
15
16pub struct CargoLock {}
17
18impl CargoLock {
19 pub fn new() -> Self {
20 Self {}
21 }
22
23 fn parse_corrupted_lockfile(path: impl AsRef<Path>) -> Result<Package, SourcePkgError> {
24 let location = path.as_ref();
25 let content = std::fs::read_to_string(location)?;
26 let root = Value::from_str(&content)?;
27 let root = match root.as_table() {
28 Some(root) => root,
29 None => {
30 return Err(SourcePkgError::GenericsError(
31 "corrupted lockfile : the root element must be table",
32 ))
33 }
34 };
35
36 let package = match root.get("package") {
37 Some(package) => package,
38 None => {
39 return Err(SourcePkgError::GenericsError(
40 "corrupted lockfile : the root element must have a package element",
41 ))
42 }
43 };
44
45 let package = match package.as_array() {
46 Some(package) => package,
47 None => {
48 return Err(SourcePkgError::GenericsError(
49 "corrupted lockfile : the package element must be array",
50 ))
51 }
52 };
53
54 let mut dependencies = Vec::new();
55 for pkg in package {
56 let pkg = match pkg.as_table() {
57 Some(pkg) => pkg,
58 None => continue,
59 };
60
61 let name = pkg.get("name").and_then(|name| name.as_str());
62
63 let name = match name {
64 Some(name) => name,
65 None => continue,
66 };
67
68 let version = pkg.get("version").and_then(|version| version.as_str());
69
70 let version = match version {
71 Some(version) => version,
72 None => continue,
73 };
74
75 let dep = DependentPackage {
76 purl: PackageUrl::new("cargo", name)
77 .expect("purl arguments are invalid")
78 .to_string(),
79
80 requirement: version.to_string(),
81 ..Default::default()
82 };
83
84 dependencies.push(dep);
85 }
86
87 let manifest = Package {
88 dependencies,
89 ..Default::default()
90 };
91
92 Ok(manifest)
93 }
94
95 fn parse_lockfile(path: impl AsRef<Path>) -> Result<Package, SourcePkgError> {
96 let lock = CargoLockfile::load(path)?;
97 let dependencies = lock
98 .packages
99 .into_iter()
100 .map(|pkg| DependentPackage {
101 purl: PackageUrl::new("cargo", pkg.name.as_str())
102 .expect("purl arguments are invalid")
103 .to_string(),
104 requirement: pkg.version.to_string(),
105 ..Default::default()
106 })
107 .collect::<Vec<_>>();
108
109 let manifest = Package {
110 dependencies,
111 ..Default::default()
112 };
113
114 Ok(manifest)
115 }
116}
117
118#[async_trait::async_trait]
119impl PackageManifest for CargoLock {
120 fn get_name(&self) -> String {
121 "crates".into()
122 }
123
124 async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
125 let manifest = match Self::parse_lockfile(path) {
126 Ok(manifest) => manifest,
127 Err(_) => Self::parse_corrupted_lockfile(path)?,
128 };
129
130 Ok(manifest)
131 }
132
133 fn file_name_patterns(&self) -> &'static [&'static str] {
134 &["Cargo.lock"]
135 }
136}
137
138pub struct CargoToml {}
139
140impl CargoToml {
141 pub fn new() -> Self {
142 Self {}
143 }
144
145 fn parse_cargo_toml(path: impl AsRef<Path>) -> Result<Package, SourcePkgError> {
146 let metadata = match CargoManifest::from_path(path) {
147 Ok(metadata) => metadata,
148 Err(err) => match err {
149 CargoManifestError::Io(err) => return Err(SourcePkgError::Io(err)),
150 CargoManifestError::Parse(err) => return Err(SourcePkgError::TomlDeserialize(err)),
151 CargoManifestError::Utf8(_) => {
152 return Err(SourcePkgError::GenericsError("invalid utf8"))
153 }
154 },
155 };
156
157 let convert = |scope: &str, is_dev: bool| {
158 let scope = scope.to_string();
159
160 move |(name, dep): (String, Dependency)| DependentPackage {
161 purl: PackageUrl::new("cargo", name)
162 .expect("purl arguments are invalid")
163 .to_string(),
164 scope: scope.clone(),
165 is_runtime: !is_dev,
166 is_optional: match &dep {
167 cargo_manifest::Dependency::Simple(_) => false,
168 cargo_manifest::Dependency::Detailed(detail) => {
169 detail.optional.unwrap_or(false)
170 }
171 },
172 is_resolved: false,
173 requirement: match dep {
174 cargo_manifest::Dependency::Simple(version) => version,
175 cargo_manifest::Dependency::Detailed(detail) => {
176 detail.version.unwrap_or_else(|| "*".into())
178 }
179 },
180 parents: Default::default(),
181 relation: hashset! {Relation::Direct},
182 reachable: Default::default(),
183 }
184 };
185
186 let dependencies = metadata
187 .dependencies
188 .unwrap_or_default()
189 .into_iter()
190 .map(convert("dependencies", false));
191
192 let dev_dependencies = metadata
193 .dev_dependencies
194 .unwrap_or_default()
195 .into_iter()
196 .map(convert("dev-dependencies", true));
197
198 let dependencies = dependencies.chain(dev_dependencies).collect();
199
200 let package = Package {
202 dependencies,
203 ..Default::default()
204 };
205
206 Ok(package)
207 }
208}
209
210#[async_trait::async_trait]
211impl PackageManifest for CargoToml {
212 fn get_name(&self) -> String {
213 "crates".into()
214 }
215
216 async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
217 Self::parse_cargo_toml(path)
218 }
219
220 fn file_name_patterns(&self) -> &'static [&'static str] {
221 &["Cargo.toml"]
222 }
223}
224
225#[cfg(test)]
226mod test {
227 use super::*;
228 use std::path::PathBuf;
229
230 #[test]
231 fn test_parser_cargo_toml() {
232 let filepath = PathBuf::from(concat!(
233 env!("CARGO_MANIFEST_DIR"),
234 "/testdata/cargo/cargo_toml/clippy/Cargo.toml"
235 ));
236 CargoToml::parse_cargo_toml(filepath).unwrap();
237 }
239
240 #[test]
241 fn test_parser_cargo_lock_1() {
242 let filepath = PathBuf::from(concat!(
243 env!("CARGO_MANIFEST_DIR"),
244 "/testdata/cargo/cargo_lock/sample5/Cargo.lock"
245 ));
246
247 CargoLock::parse_corrupted_lockfile(filepath).unwrap();
248 }
249
250 #[test]
251 fn test_parser_cargo_lock_2() {
252 let filepath = PathBuf::from(concat!(
253 env!("CARGO_MANIFEST_DIR"),
254 "/testdata/cargo/cargo_lock/sample6/Cargo.lock"
255 ));
256
257 CargoLock::parse_lockfile(filepath).unwrap();
258 }
259}