|
| 1 | +use std::collections::HashMap; |
| 2 | + |
| 3 | +use auditable_serde::{Package, Source, VersionInfo}; |
| 4 | +use cargo_metadata::DependencyKind; |
| 5 | +use cargo_util_schemas::core::{PackageIdSpec, SourceKind}; |
| 6 | +use serde::{Deserialize, Serialize}; |
| 7 | + |
| 8 | +/// Cargo SBOM precursor format. |
| 9 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 10 | +pub struct SbomPrecursor { |
| 11 | + /// Schema version |
| 12 | + pub version: u32, |
| 13 | + /// Index into the crates array for the root crate |
| 14 | + pub root: usize, |
| 15 | + /// Array of all crates |
| 16 | + pub crates: Vec<Crate>, |
| 17 | + /// Information about rustc used to perform the compilation |
| 18 | + pub rustc: RustcInfo, |
| 19 | +} |
| 20 | + |
| 21 | +impl From<SbomPrecursor> for VersionInfo { |
| 22 | + fn from(sbom: SbomPrecursor) -> Self { |
| 23 | + // cargo sbom data format has more nodes than the auditable info format - if a crate is both a build |
| 24 | + // and runtime dependency it will appear twice in the `crates` array. |
| 25 | + // The `VersionInfo` format lists each package only once, with a single `kind` field |
| 26 | + // (Runtime having precence over other kinds). |
| 27 | + |
| 28 | + // Firstly, we deduplicate the (name, version) pairs and create a mapping from the |
| 29 | + // original indices in the cargo sbom array to the new index in the auditable info package array. |
| 30 | + let (_, mut packages, indices) = sbom.crates.iter().enumerate().fold( |
| 31 | + (HashMap::new(), Vec::new(), Vec::new()), |
| 32 | + |(mut id_to_index_map, mut packages, mut indices), (index, crate_)| { |
| 33 | + match id_to_index_map.entry(crate_.id.clone()) { |
| 34 | + std::collections::hash_map::Entry::Occupied(entry) => { |
| 35 | + // Just store the new index in the indices array |
| 36 | + indices.push(*entry.get()); |
| 37 | + } |
| 38 | + std::collections::hash_map::Entry::Vacant(entry) => { |
| 39 | + // If the entry does not exist, we create it |
| 40 | + 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 | + }, |
| 49 | + // Assume build, if we determine this is a runtime dependency we'll update later |
| 50 | + kind: auditable_serde::DependencyKind::Build, |
| 51 | + // We will fill this in later |
| 52 | + dependencies: Vec::new(), |
| 53 | + root: index == sbom.root, |
| 54 | + }); |
| 55 | + entry.insert(packages.len() - 1); |
| 56 | + indices.push(packages.len() - 1); |
| 57 | + } |
| 58 | + } |
| 59 | + (id_to_index_map, packages, indices) |
| 60 | + }, |
| 61 | + ); |
| 62 | + |
| 63 | + // Traverse the graph as given by the sbom to fill in the dependencies with the new indices. |
| 64 | + // |
| 65 | + // Keep track of whether the dependency is a runtime dependency. |
| 66 | + // If we ever encounter a non-runtime dependency, all deps in the remaining subtree |
| 67 | + // are not runtime dependencies, i.e a runtime dep of a build dep is not recognized as a runtime dep. |
| 68 | + let mut stack = Vec::new(); |
| 69 | + stack.push((sbom.root, true)); |
| 70 | + while let Some((old_index, is_runtime)) = stack.pop() { |
| 71 | + let crate_ = &sbom.crates[old_index]; |
| 72 | + for dep in &crate_.dependencies { |
| 73 | + stack.push((dep.index, dep.kind == DependencyKind::Normal && is_runtime)); |
| 74 | + } |
| 75 | + |
| 76 | + let package = &mut packages[indices[old_index]]; |
| 77 | + if is_runtime { |
| 78 | + package.kind = auditable_serde::DependencyKind::Runtime |
| 79 | + }; |
| 80 | + |
| 81 | + for dep in &crate_.dependencies { |
| 82 | + let new_dep_index = indices[dep.index]; |
| 83 | + if package.dependencies.contains(&new_dep_index) { |
| 84 | + continue; // Already added this dependency |
| 85 | + } else if new_dep_index == indices[old_index] { |
| 86 | + // If the dependency is the same as the package itself, skip it |
| 87 | + continue; |
| 88 | + } else { |
| 89 | + package.dependencies.push(new_dep_index); |
| 90 | + } |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + VersionInfo { packages } |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 99 | +pub struct Crate { |
| 100 | + /// Package ID specification |
| 101 | + pub id: PackageIdSpec, |
| 102 | + /// List of target kinds |
| 103 | + pub kind: Vec<String>, |
| 104 | + /// Enabled feature flags |
| 105 | + pub features: Vec<String>, |
| 106 | + /// Dependencies for this crate |
| 107 | + pub dependencies: Vec<Dependency>, |
| 108 | +} |
| 109 | + |
| 110 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 111 | +pub struct Dependency { |
| 112 | + /// Index into the crates array |
| 113 | + pub index: usize, |
| 114 | + /// Dependency kind: "normal", "build", or "dev" |
| 115 | + pub kind: DependencyKind, |
| 116 | +} |
| 117 | + |
| 118 | +#[derive(Debug, Clone, Serialize, Deserialize)] |
| 119 | +pub struct RustcInfo { |
| 120 | + /// Compiler version |
| 121 | + pub version: String, |
| 122 | + /// Compiler wrapper |
| 123 | + pub wrapper: Option<String>, |
| 124 | + /// Compiler workspace wrapper |
| 125 | + pub workspace_wrapper: Option<String>, |
| 126 | + /// Commit hash for rustc |
| 127 | + pub commit_hash: String, |
| 128 | + /// Host target triple |
| 129 | + pub host: String, |
| 130 | + /// Verbose version string: `rustc -vV` |
| 131 | + pub verbose_version: String, |
| 132 | +} |
0 commit comments