Skip to main content

package_parser/pkgs/
cargo.rs

1use 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                        // arbitrary version for None case
177                        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        // TODO: add authors
201        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        // println!("{:#?}", metadata);
238    }
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}