|
1 | 1 | use std::collections::HashMap; |
2 | 2 |
|
3 | 3 | use auditable_serde::{Package, Source, VersionInfo}; |
4 | | -use cargo_metadata::DependencyKind; |
5 | | -use cargo_util_schemas::core::{PackageIdSpec, SourceKind}; |
| 4 | +use cargo_metadata::{ |
| 5 | + semver::{self, Version}, |
| 6 | + DependencyKind, |
| 7 | +}; |
6 | 8 | use serde::{Deserialize, Serialize}; |
7 | 9 |
|
8 | 10 | /// Cargo SBOM precursor format. |
@@ -36,16 +38,12 @@ impl From<SbomPrecursor> for VersionInfo { |
36 | 38 | indices.push(*entry.get()); |
37 | 39 | } |
38 | 40 | std::collections::hash_map::Entry::Vacant(entry) => { |
| 41 | + let (name, version, source) = parse_fully_qualified_package_id(&crate_.id); |
39 | 42 | // If the entry does not exist, we create it |
40 | 43 | packages.push(Package { |
41 | | - name: crate_.id.name().to_string(), |
42 | | - version: crate_.id.version().expect("Package to have version"), |
43 | | - source: match crate_.id.kind() { |
44 | | - Some(SourceKind::Path) => Source::Local, |
45 | | - Some(SourceKind::Git(_)) => Source::Git, |
46 | | - Some(_) => Source::Registry, |
47 | | - None => Source::CratesIo, |
48 | | - }, |
| 44 | + name, |
| 45 | + version, |
| 46 | + source, |
49 | 47 | // Assume build, if we determine this is a runtime dependency we'll update later |
50 | 48 | kind: auditable_serde::DependencyKind::Build, |
51 | 49 | // We will fill this in later |
@@ -98,7 +96,7 @@ impl From<SbomPrecursor> for VersionInfo { |
98 | 96 | #[derive(Debug, Clone, Serialize, Deserialize)] |
99 | 97 | pub struct Crate { |
100 | 98 | /// Package ID specification |
101 | | - pub id: PackageIdSpec, |
| 99 | + pub id: String, |
102 | 100 | /// List of target kinds |
103 | 101 | pub kind: Vec<String>, |
104 | 102 | /// Enabled feature flags |
@@ -130,3 +128,72 @@ pub struct RustcInfo { |
130 | 128 | /// Verbose version string: `rustc -vV` |
131 | 129 | pub verbose_version: String, |
132 | 130 | } |
| 131 | + |
| 132 | +const CRATES_IO_INDEX: &str = "https://github.com/rust-lang/crates.io-index"; |
| 133 | + |
| 134 | +/// Parses a fully qualified package ID spec string into a tuple of (name, version, source). |
| 135 | +/// The package ID spec format is defined at https://doc.rust-lang.org/cargo/reference/pkgid-spec.html#package-id-specifications-1 |
| 136 | +/// |
| 137 | +/// The fully qualified form of a package ID spec is mentioned in the Cargo documentation, |
| 138 | +/// figuring it out is left as an exercise to the reader. |
| 139 | +/// |
| 140 | +/// Adapting the grammar in the cargo doc, the format appears to be : |
| 141 | +/// ```norust |
| 142 | +/// fully_qualified_spec := kind "+" proto "://" hostname-and-path [ "?" query] "#" [ name "@" ] semver |
| 143 | +/// query := ( "branch" | "tag" | "rev" ) "=" ref |
| 144 | +/// semver := digits "." digits "." digits [ "-" prerelease ] [ "+" build ] |
| 145 | +/// kind := "registry" | "git" | "path" |
| 146 | +/// proto := "http" | "git" | "file" | ... |
| 147 | +/// ``` |
| 148 | +/// where: |
| 149 | +/// - the name is always present except when the kind is `path` and the last segment of the path doesn't match the name |
| 150 | +/// - the query string is only present for git dependencies (which we can ignore since we don't record git information) |
| 151 | +fn parse_fully_qualified_package_id(id: &str) -> (String, Version, Source) { |
| 152 | + let (kind, rest) = id.split_once('+').expect("Package ID to have a kind"); |
| 153 | + let (url, rest) = rest |
| 154 | + .split_once('#') |
| 155 | + .expect("Package ID to have version information"); |
| 156 | + let source = match (kind, url) { |
| 157 | + ("registry", CRATES_IO_INDEX) => Source::CratesIo, |
| 158 | + ("registry", _) => Source::Registry, |
| 159 | + ("git", _) => Source::Git, |
| 160 | + ("path", _) => Source::Local, |
| 161 | + _ => Source::Other(kind.to_string()), |
| 162 | + }; |
| 163 | + |
| 164 | + if source == Source::Local { |
| 165 | + // For local packages, the name might be in the suffix after '#' if it has |
| 166 | + // a diferent name than the last segment of the path. |
| 167 | + if let Some((name, version)) = rest.split_once('@') { |
| 168 | + ( |
| 169 | + name.to_string(), |
| 170 | + semver::Version::parse(version).expect("Version to be valid SemVer"), |
| 171 | + source, |
| 172 | + ) |
| 173 | + } else { |
| 174 | + // If no name is specified, use the last segment of the path as the name |
| 175 | + let name = url |
| 176 | + .split('/') |
| 177 | + .next_back() |
| 178 | + .unwrap() |
| 179 | + .split('\\') |
| 180 | + .next_back() |
| 181 | + .unwrap(); |
| 182 | + ( |
| 183 | + name.to_string(), |
| 184 | + semver::Version::parse(rest).expect("Version to be valid SemVer"), |
| 185 | + source, |
| 186 | + ) |
| 187 | + } |
| 188 | + } else { |
| 189 | + // For other sources, the name and version are after the '#', separated by '@' |
| 190 | + let (name, version) = rest |
| 191 | + .split_once('@') |
| 192 | + .expect("Package ID to have a name and version"); |
| 193 | + ( |
| 194 | + name.to_string(), |
| 195 | + semver::Version::parse(version).expect("Version to be valid SemVer"), |
| 196 | + source, |
| 197 | + ) |
| 198 | + } |
| 199 | +} |
0 commit comments