Skip to content

Commit 37cd4fb

Browse files
committed
parse fully qualified package ID specs from SBOMs
1 parent 15cbd7b commit 37cd4fb

File tree

10 files changed

+165
-383
lines changed

10 files changed

+165
-383
lines changed

Cargo.lock

Lines changed: 5 additions & 371 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cargo-auditable/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ cargo_metadata = "0.18"
2121
pico-args = { version = "0.5", features = ["eq-separator", "short-space-opt"] }
2222
serde = "1.0.147"
2323
wasm-gen = "0.1.4"
24-
cargo-util-schemas = "0.8.1"
2524

2625
[dev-dependencies]
2726
cargo_metadata = "0.18"

cargo-auditable/src/sbom_precursor.rs

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use std::collections::HashMap;
22

33
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+
};
68
use serde::{Deserialize, Serialize};
79

810
/// Cargo SBOM precursor format.
@@ -36,16 +38,12 @@ impl From<SbomPrecursor> for VersionInfo {
3638
indices.push(*entry.get());
3739
}
3840
std::collections::hash_map::Entry::Vacant(entry) => {
41+
let (name, version, source) = parse_fully_qualified_package_id(&crate_.id);
3942
// If the entry does not exist, we create it
4043
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,
4947
// Assume build, if we determine this is a runtime dependency we'll update later
5048
kind: auditable_serde::DependencyKind::Build,
5149
// We will fill this in later
@@ -98,7 +96,7 @@ impl From<SbomPrecursor> for VersionInfo {
9896
#[derive(Debug, Clone, Serialize, Deserialize)]
9997
pub struct Crate {
10098
/// Package ID specification
101-
pub id: PackageIdSpec,
99+
pub id: String,
102100
/// List of target kinds
103101
pub kind: Vec<String>,
104102
/// Enabled feature flags
@@ -130,3 +128,72 @@ pub struct RustcInfo {
130128
/// Verbose version string: `rustc -vV`
131129
pub verbose_version: String,
132130
}
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+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "bar"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
8+
[workspace]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
pub fn add(left: u64, right: u64) -> u64 {
2+
left + right
3+
}
4+
5+
#[cfg(test)]
6+
mod tests {
7+
use super::*;
8+
9+
#[test]
10+
fn it_works() {
11+
let result = add(2, 2);
12+
assert_eq!(result, 4);
13+
}
14+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "foo"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
bar = { version = "0.1.0", path = "../bar" }
8+
baz = { version = "0.1.0", path = "../qux" }
9+
10+
[workspace]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
println!("Hello, world!");
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "baz"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
8+
[workspace]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
pub fn add(left: u64, right: u64) -> u64 {
2+
left + right
3+
}
4+
5+
#[cfg(test)]
6+
mod tests {
7+
use super::*;
8+
9+
#[test]
10+
fn it_works() {
11+
let result = add(2, 2);
12+
assert_eq!(result, 4);
13+
}
14+
}

cargo-auditable/tests/it.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,3 +527,28 @@ fn test_wasm_inner(sbom: bool) {
527527
eprintln!("wasm_crate.wasm dependency info: {dep_info:?}");
528528
assert_eq!(dep_info.packages.len(), 16);
529529
}
530+
531+
#[test]
532+
fn test_path_not_equal_name() {
533+
test_path_not_equal_name_inner(false);
534+
test_path_not_equal_name_inner(true);
535+
}
536+
537+
fn test_path_not_equal_name_inner(sbom: bool) {
538+
// This tests a case where a path dependency's directory name is not equal to the crate name.
539+
let cargo_toml = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
540+
.join("tests/fixtures/path_not_equal_name/foo/Cargo.toml");
541+
let bins = run_cargo_auditable(cargo_toml, &[], &[], sbom);
542+
let foo_bin = &bins.get("foo").unwrap()[0];
543+
let dep_info = get_dependency_info(foo_bin);
544+
eprintln!("{foo_bin} dependency info: {dep_info:?}");
545+
assert!(dep_info.packages.len() == 3);
546+
assert!(dep_info
547+
.packages
548+
.iter()
549+
.any(|p| p.name == "bar" && p.kind == DependencyKind::Runtime));
550+
assert!(dep_info
551+
.packages
552+
.iter()
553+
.any(|p| p.name == "baz" && p.kind == DependencyKind::Runtime));
554+
}

0 commit comments

Comments
 (0)