use std::collections::HashMap;
use auditable_serde::{Package, Source, VersionInfo};
use cargo_metadata::{
semver::{self, Version},
DependencyKind,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SbomPrecursor {
pub version: u32,
pub root: usize,
pub crates: Vec<Crate>,
pub rustc: RustcInfo,
}
impl From<SbomPrecursor> for VersionInfo {
fn from(sbom: SbomPrecursor) -> Self {
let (_, mut packages, indices) = sbom.crates.iter().enumerate().fold(
(HashMap::new(), Vec::new(), Vec::new()),
|(mut id_to_index_map, mut packages, mut indices), (index, crate_)| {
match id_to_index_map.entry(crate_.id.clone()) {
std::collections::hash_map::Entry::Occupied(entry) => {
indices.push(*entry.get());
}
std::collections::hash_map::Entry::Vacant(entry) => {
let (name, version, source) = parse_fully_qualified_package_id(&crate_.id);
packages.push(Package {
name,
version,
source,
kind: auditable_serde::DependencyKind::Build,
dependencies: Vec::new(),
root: index == sbom.root,
});
entry.insert(packages.len() - 1);
indices.push(packages.len() - 1);
}
}
(id_to_index_map, packages, indices)
},
);
let mut stack = Vec::new();
stack.push((sbom.root, true));
while let Some((old_index, is_runtime)) = stack.pop() {
let crate_ = &sbom.crates[old_index];
for dep in &crate_.dependencies {
stack.push((dep.index, dep.kind == DependencyKind::Normal && is_runtime));
}
let package = &mut packages[indices[old_index]];
if is_runtime {
package.kind = auditable_serde::DependencyKind::Runtime
};
for dep in &crate_.dependencies {
let new_dep_index = indices[dep.index];
if package.dependencies.contains(&new_dep_index) {
continue; } else if new_dep_index == indices[old_index] {
continue;
} else {
package.dependencies.push(new_dep_index);
}
}
}
VersionInfo {
packages,
format: 8,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Crate {
pub id: String,
pub kind: Vec<String>,
pub features: Vec<String>,
pub dependencies: Vec<Dependency>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
pub index: usize,
pub kind: DependencyKind,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RustcInfo {
pub version: String,
pub wrapper: Option<String>,
pub workspace_wrapper: Option<String>,
pub commit_hash: String,
pub host: String,
pub verbose_version: String,
}
const CRATES_IO_INDEX: &str = "https://github.com/rust-lang/crates.io-index";
fn parse_fully_qualified_package_id(id: &str) -> (String, Version, Source) {
let (kind, rest) = id.split_once('+').expect("Package ID to have a kind");
let (url, rest) = rest
.split_once('#')
.expect("Package ID to have version information");
let source = match (kind, url) {
("registry", CRATES_IO_INDEX) => Source::CratesIo,
("registry", _) => Source::Registry,
("git", _) => Source::Git,
("path", _) => Source::Local,
_ => Source::Other(kind.to_string()),
};
if let Some((name, version)) = rest.split_once('@') {
(
name.to_string(),
semver::Version::parse(version).expect("Version to be valid SemVer"),
source,
)
} else {
let path = url.split_once('?').map(|(p, _)| p).unwrap_or(url);
let name = path
.rsplit(['/', '\\'])
.next()
.filter(|segment| !segment.is_empty())
.expect("Package ID URL to end with a package name");
(
name.to_string(),
semver::Version::parse(rest).expect("Version to be valid SemVer"),
source,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_id(id: &str, expected_name: &str, expected_version: &str, expected_source: Source) {
let (name, version, source) = parse_fully_qualified_package_id(id);
assert_eq!(name, expected_name, "name mismatch for {id}");
assert_eq!(
version.to_string(),
expected_version,
"version mismatch for {id}"
);
assert_eq!(source, expected_source, "source mismatch for {id}");
}
#[test]
fn registry_with_name() {
assert_id(
"registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.8.16",
"zerocopy",
"0.8.16",
Source::CratesIo,
);
}
#[test]
fn path_with_elided_name() {
assert_id(
"path+file:///tmp/sample-package#0.1.0",
"sample-package",
"0.1.0",
Source::Local,
);
}
#[test]
fn path_with_explicit_name() {
assert_id(
"path+file:///tmp/some-dir#different-name@0.1.0",
"different-name",
"0.1.0",
Source::Local,
);
}
#[test]
fn git_with_explicit_name() {
assert_id(
"git+https://github.com/rayon-rs/rayon?branch=main#rayon-core@1.13.0",
"rayon-core",
"1.13.0",
Source::Git,
);
}
#[test]
fn git_with_elided_name() {
assert_id(
"git+https://github.com/rayon-rs/rayon?branch=main#1.11.0",
"rayon",
"1.11.0",
Source::Git,
);
}
#[test]
fn git_with_elided_name_no_query() {
assert_id(
"git+https://github.com/rayon-rs/rayon#1.11.0",
"rayon",
"1.11.0",
Source::Git,
);
}
}