use crate::Bom;
use crate::Metadata;
use cargo::core::dependency::DepKind;
use cargo::core::Package;
use cargo::core::PackageSet;
use cargo::core::Resolve;
use cargo::core::Workspace;
use cargo::ops;
use cargo::Config;
use std::collections::{BTreeSet, HashMap};
use std::path::{Path, PathBuf};
use thiserror::Error;
pub trait Generator {
fn create_sboms<'a>(
&self,
manifest_path: PathBuf,
) -> Result<HashMap<PathBuf, Bom>, GeneratorError>;
}
pub struct SbomGenerator {
pub all: bool,
}
impl Generator for SbomGenerator {
fn create_sboms<'a>(
&self,
manifest_path: PathBuf,
) -> Result<HashMap<PathBuf, Bom>, GeneratorError> {
let config_filepath = manifest_path.to_string_lossy().to_string();
let config = Config::default().map_err(|error| GeneratorError::CargoConfigError {
config_filepath: config_filepath.clone(),
error,
})?;
let ws = Workspace::new(&manifest_path, &config).map_err(|error| {
GeneratorError::CargoConfigError {
config_filepath: config_filepath.clone(),
error,
}
})?;
let members: Vec<Package> = ws.members().cloned().collect();
let (package_ids, resolve) =
ops::resolve_ws(&ws).map_err(|error| GeneratorError::CargoConfigError {
config_filepath: config_filepath.clone(),
error,
})?;
let mut result = HashMap::with_capacity(members.len());
for member in members.iter() {
let dependencies = if self.all {
all_dependencies(&members, &package_ids, &resolve)?
} else {
top_level_dependencies(&members, &package_ids)?
};
let mut bom: Bom = dependencies.iter().collect();
bom.metadata = get_metadata(member.manifest_path());
result.insert(member.manifest_path().to_path_buf(), bom);
}
Ok(result)
}
}
fn get_metadata(toml_file_path: &Path) -> Option<Metadata> {
let metadata = cargo_metadata::MetadataCommand::new()
.manifest_path(&toml_file_path)
.features(cargo_metadata::CargoOpt::AllFeatures)
.exec();
match metadata {
Ok(metadata) => metadata
.root_package()
.map(Metadata::from)
.or_else(|| Some(Metadata::default())),
Err(e) => {
log::error!(
"Attempted to get metadata from the cargo config: {}",
toml_file_path.to_string_lossy(),
);
log::debug!("Got error: {}", e);
Some(Metadata::default())
}
}
}
fn top_level_dependencies(
members: &[Package],
package_ids: &PackageSet<'_>,
) -> Result<BTreeSet<Package>, GeneratorError> {
let mut dependencies = BTreeSet::new();
let all_dependencies = members
.iter()
.flat_map(|m| m.dependencies().iter())
.filter(|d| d.kind() == DepKind::Normal);
for dependency in all_dependencies {
if let Some(package_id) = package_ids
.package_ids()
.find(|id| dependency.matches_id(*id))
{
let package = package_ids
.get_one(package_id)
.map_err(|error| GeneratorError::PackageError { package_id, error })?;
dependencies.insert(package.to_owned());
}
}
for member in members {
dependencies.remove(member);
}
Ok(dependencies)
}
fn all_dependencies(
members: &[Package],
package_ids: &PackageSet<'_>,
resolve: &Resolve,
) -> Result<BTreeSet<Package>, GeneratorError> {
let mut dependencies = BTreeSet::new();
for package_id in resolve.iter() {
let package = package_ids
.get_one(package_id)
.map_err(|error| GeneratorError::PackageError { package_id, error })?;
if members.contains(&package) {
continue;
}
dependencies.insert(package.to_owned());
}
Ok(dependencies)
}
#[derive(Error, Debug)]
pub enum GeneratorError {
#[error("Expected a root package in the cargo config: {config_filepath}")]
RootPackageMissingError { config_filepath: String },
#[error("Could not process the cargo config: {config_filepath}")]
CargoConfigError {
config_filepath: String,
#[source]
error: anyhow::Error,
},
#[error("Error with Cargo metadata")]
CargoMetaDataError(#[from] cargo_metadata::Error),
#[error("Error retrieving package information: {package_id}")]
PackageError {
package_id: cargo::core::package_id::PackageId,
#[source]
error: anyhow::Error,
},
}