use std::collections::HashSet;
use std::path::PathBuf;
use crate::output_schema::{Output, OutputFileInfo as FileInfo, OutputPackage};
use super::OutputWriteConfig;
use super::sbom::SbomInventory;
#[derive(Clone, Copy)]
pub(crate) enum SpdxPackageRole {
DocumentSubject,
PromotedDependency,
}
impl SpdxPackageRole {
pub(crate) fn files_analyzed(self) -> bool {
matches!(self, Self::DocumentSubject)
}
pub(crate) fn is_described_by_document(self) -> bool {
matches!(self, Self::DocumentSubject)
}
}
pub(crate) struct SpdxPackagePlan<'a> {
pub spdx_id: String,
pub name: String,
pub files: Vec<&'a FileInfo>,
pub package: Option<&'a OutputPackage>,
pub role: SpdxPackageRole,
}
pub(crate) fn inventory_spdx_ids(inventory: &SbomInventory<'_>) -> Vec<String> {
let mut detected = 0usize;
let mut promoted = 0usize;
inventory
.entries
.iter()
.map(|entry| {
if entry.is_promoted() {
promoted += 1;
format!("SPDXRef-Package-Dependency-{promoted}")
} else {
detected += 1;
format!("SPDXRef-Package-{detected}")
}
})
.collect()
}
pub(crate) fn plan_spdx_packages<'a>(
output: &'a Output,
files: &[&'a FileInfo],
config: &OutputWriteConfig,
inventory: &'a SbomInventory<'a>,
) -> Vec<SpdxPackagePlan<'a>> {
if inventory.is_empty() {
return vec![SpdxPackagePlan {
spdx_id: "SPDXRef-001".to_string(),
name: primary_package_name(output, config),
files: files.to_vec(),
package: None,
role: SpdxPackageRole::DocumentSubject,
}];
}
let mut assigned_paths: HashSet<&str> = HashSet::new();
let mut plans = Vec::with_capacity(inventory.entries.len() + 1);
for (detected_idx, entry) in inventory
.entries
.iter()
.filter(|entry| !entry.is_promoted())
.enumerate()
{
let package = entry.package();
let owned: Vec<&FileInfo> = files
.iter()
.copied()
.filter(|file| {
file.for_packages
.iter()
.any(|uid| uid == &package.package_uid)
})
.collect();
for file in &owned {
assigned_paths.insert(file.path.as_str());
}
plans.push(SpdxPackagePlan {
spdx_id: format!("SPDXRef-Package-{}", detected_idx + 1),
name: spdx_assembled_package_name(package, detected_idx),
files: owned,
package: Some(package),
role: SpdxPackageRole::DocumentSubject,
});
}
let unassigned: Vec<&FileInfo> = files
.iter()
.copied()
.filter(|file| !assigned_paths.contains(file.path.as_str()))
.collect();
if !unassigned.is_empty() {
plans.push(SpdxPackagePlan {
spdx_id: "SPDXRef-Package-unassigned".to_string(),
name: primary_package_name(output, config),
files: unassigned,
package: None,
role: SpdxPackageRole::DocumentSubject,
});
}
for (promoted_idx, entry) in inventory
.entries
.iter()
.filter(|entry| entry.is_promoted())
.enumerate()
{
let package = entry.package();
plans.push(SpdxPackagePlan {
spdx_id: format!("SPDXRef-Package-Dependency-{}", promoted_idx + 1),
name: spdx_assembled_package_name(package, promoted_idx),
files: Vec::new(),
package: Some(package),
role: SpdxPackageRole::PromotedDependency,
});
}
plans
}
pub(crate) fn primary_package_name(output: &Output, config: &OutputWriteConfig) -> String {
if output.packages.len() == 1
&& let Some(name) = output.packages.first().and_then(|p| p.name.clone())
{
return sanitize_spdx_package_name(&name);
}
if let Some(scanned_path) = &config.scanned_path {
let path = PathBuf::from(scanned_path);
if let Some(name) = path.file_name().and_then(|n| n.to_str())
&& !name.is_empty()
{
return sanitize_spdx_package_name(name);
}
}
output
.packages
.first()
.and_then(|p| p.name.clone())
.map(|name| sanitize_spdx_package_name(&name))
.unwrap_or_else(|| "provenant-analyzed-package".to_string())
}
pub(crate) fn sanitize_spdx_package_name(name: &str) -> String {
let mut out = String::with_capacity(name.len());
for ch in name.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == '.' {
out.push(ch);
} else {
out.push('_');
}
}
if out.is_empty() {
"provenant-analyzed-package".to_string()
} else {
out
}
}
fn spdx_assembled_package_name(package: &OutputPackage, idx: usize) -> String {
package
.name
.as_deref()
.filter(|name| !name.is_empty())
.map(sanitize_spdx_package_name)
.unwrap_or_else(|| format!("package-{}", idx + 1))
}