Skip to content

Commit 1e8cf8f

Browse files
committed
index dependency kinds correctly
Signed-off-by: Markus Theil <theil.markus@gmail.com>
1 parent 6a1c95a commit 1e8cf8f

File tree

1 file changed

+95
-39
lines changed

1 file changed

+95
-39
lines changed

cargo-cyclonedx/src/generator.rs

Lines changed: 95 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::config::Describe;
2+
use std::cmp::min;
23
/*
34
* This file is part of CycloneDX Rust Cargo.
45
*
@@ -54,8 +55,8 @@ use once_cell::sync::Lazy;
5455
use regex::Regex;
5556

5657
use log::Level;
57-
use std::collections::BTreeMap;
5858
use std::collections::HashMap;
59+
use std::collections::{BTreeMap, LinkedList};
5960
use std::convert::TryFrom;
6061
use std::fs::File;
6162
use std::io::BufWriter;
@@ -68,7 +69,42 @@ use validator::validate_email;
6869
// Maps from PackageId to Package for efficiency - faster lookups than in a Vec
6970
type PackageMap = BTreeMap<PackageId, Package>;
7071
type ResolveMap = BTreeMap<PackageId, Node>;
71-
type DependencyKindMap = BTreeMap<PackageId, Vec<DependencyKind>>;
72+
type DependencyKindMap = BTreeMap<PackageId, DependencyKind>;
73+
74+
/// The values are ordered from weakest to strongest so that casting to integer would make sense
75+
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
76+
enum PrivateDepKind {
77+
Development,
78+
Build,
79+
Runtime,
80+
}
81+
impl From<PrivateDepKind> for DependencyKind {
82+
fn from(priv_kind: PrivateDepKind) -> Self {
83+
match priv_kind {
84+
PrivateDepKind::Development => DependencyKind::Development,
85+
PrivateDepKind::Build => DependencyKind::Build,
86+
PrivateDepKind::Runtime => DependencyKind::Normal,
87+
}
88+
}
89+
}
90+
91+
impl From<&DependencyKind> for PrivateDepKind {
92+
fn from(kind: &DependencyKind) -> Self {
93+
match kind {
94+
DependencyKind::Normal => PrivateDepKind::Runtime,
95+
DependencyKind::Development => PrivateDepKind::Development,
96+
DependencyKind::Build => PrivateDepKind::Build,
97+
_ => panic!("Unknown dependency kind"),
98+
}
99+
}
100+
}
101+
102+
fn strongest_dep_kind(deps: &[cargo_metadata::DepKindInfo]) -> PrivateDepKind {
103+
deps.iter()
104+
.map(|d| PrivateDepKind::from(&d.kind))
105+
.max()
106+
.unwrap_or(PrivateDepKind::Runtime) // for compatibility with Rust earlier than 1.41
107+
}
72108

73109
pub struct SbomGenerator {
74110
config: SbomConfig,
@@ -99,13 +135,13 @@ impl SbomGenerator {
99135
for member in members.iter() {
100136
log::trace!("Processing the package {}", member);
101137

102-
let mut dep_kinds = DependencyKindMap::new();
138+
let dep_kinds = index_dep_kinds(member, &resolve);
103139

104140
let (dependencies, pruned_resolve) =
105141
if config.included_dependencies() == IncludedDependencies::AllDependencies {
106-
all_dependencies(member, &packages, &resolve, &mut dep_kinds, config)
142+
all_dependencies(member, &packages, &resolve, config)
107143
} else {
108-
top_level_dependencies(member, &packages, &resolve, &mut dep_kinds, config)
144+
top_level_dependencies(member, &packages, &resolve, config)
109145
};
110146

111147
let manifest_path = packages[member].manifest_path.clone().into_std_path_buf();
@@ -199,14 +235,12 @@ impl SbomGenerator {
199235
);
200236

201237
component.purl = purl;
202-
component.scope = if let Some(deps) = dep_kinds.get(&package.id) {
203-
if deps.iter().any(|d| *d == DependencyKind::Normal) {
204-
Some(Scope::Required)
205-
} else {
206-
Some(Scope::Excluded)
207-
}
208-
} else {
209-
Some(Scope::Required)
238+
component.scope = match dep_kinds
239+
.get(&package.id)
240+
.unwrap_or(&DependencyKind::Normal)
241+
{
242+
DependencyKind::Normal => Some(Scope::Required),
243+
_ => Some(Scope::Excluded),
210244
};
211245
component.external_references = Self::get_external_references(package);
212246
component.licenses = self.get_licenses(package);
@@ -559,6 +593,49 @@ fn index_resolve(packages: Vec<Node>) -> ResolveMap {
559593
.collect()
560594
}
561595

596+
fn index_dep_kinds(root: &PackageId, resolve: &ResolveMap) -> DependencyKindMap {
597+
// cache strongest found dependency kind for every node
598+
let mut id_to_dep_kind: HashMap<&PackageId, PrivateDepKind> = HashMap::new();
599+
id_to_dep_kind.insert(root, PrivateDepKind::Runtime);
600+
601+
let root_node = &resolve[root];
602+
let mut nodes_to_visit = LinkedList::<&Node>::new();
603+
nodes_to_visit.push_front(root_node);
604+
605+
// perform a simple iterative DFS over the dependencies,
606+
// mark child deps with the minimum of parent kind and their own strongest value
607+
// therefore e.g. mark decendants of build dependencies as build dependencies,
608+
// as long as they never occur as normal dependency.
609+
while !nodes_to_visit.is_empty() {
610+
let node = nodes_to_visit.pop_front().unwrap();
611+
let parent_node_kind = id_to_dep_kind[&node.id];
612+
613+
for child_dep in &node.deps {
614+
let child_node = &resolve[&child_dep.pkg];
615+
let dep_kind = strongest_dep_kind(child_dep.dep_kinds.as_slice());
616+
let child_node_kind = min(parent_node_kind, dep_kind);
617+
618+
let dep_kind_on_previous_visit = id_to_dep_kind.get(&child_node.id);
619+
// insert/update a nodes dependency kind, when its new or stronger than the previous value
620+
if dep_kind_on_previous_visit.is_none()
621+
|| child_node_kind > *dep_kind_on_previous_visit.unwrap()
622+
{
623+
let _ = id_to_dep_kind.insert(&child_node.id, child_node_kind);
624+
}
625+
626+
// cargo metadata already return a DAG, so assume this will terminate
627+
// and don't mark already visited notes. It's ok and necessary to visit
628+
// deps as often as they occur.
629+
nodes_to_visit.push_front(child_node);
630+
}
631+
}
632+
633+
id_to_dep_kind
634+
.iter()
635+
.map(|(x, y)| ((*x).clone(), DependencyKind::from(*y)))
636+
.collect()
637+
}
638+
562639
#[derive(Error, Debug)]
563640
pub enum GeneratorError {
564641
#[error("Expected a root package in the cargo config: {config_filepath}")]
@@ -601,13 +678,12 @@ fn top_level_dependencies(
601678
root: &PackageId,
602679
packages: &PackageMap,
603680
resolve: &ResolveMap,
604-
dep_kinds: &mut DependencyKindMap,
605681
config: &SbomConfig,
606682
) -> (PackageMap, ResolveMap) {
607683
log::trace!("Adding top-level dependencies to SBOM");
608684

609685
// Only include packages that have dependency kinds other than "Development"
610-
let root_node = add_filtered_dependencies(&resolve[root], config, dep_kinds);
686+
let root_node = add_filtered_dependencies(&resolve[root], config);
611687

612688
let mut pkg_result = PackageMap::new();
613689

@@ -635,7 +711,6 @@ fn all_dependencies(
635711
root: &PackageId,
636712
packages: &PackageMap,
637713
resolve: &ResolveMap,
638-
dep_kinds: &mut DependencyKindMap,
639714
config: &SbomConfig,
640715
) -> (PackageMap, ResolveMap) {
641716
log::trace!("Adding all dependencies to SBOM");
@@ -657,14 +732,10 @@ fn all_dependencies(
657732
// If we haven't processed this node yet...
658733
if !out_resolve.contains_key(&node.id) {
659734
// Add the node to the output
660-
out_resolve.insert(
661-
node.id.to_owned(),
662-
add_filtered_dependencies(node, config, dep_kinds),
663-
);
735+
out_resolve.insert(node.id.to_owned(), add_filtered_dependencies(node, config));
664736
// Queue its dependencies for the next BFS loop iteration
665737
next_queue.extend(
666-
filtered_dependencies(&node.deps, config, dep_kinds)
667-
.map(|dep| &resolve[&dep.pkg]),
738+
filtered_dependencies(&node.deps, config).map(|dep| &resolve[&dep.pkg]),
668739
);
669740
}
670741
}
@@ -681,15 +752,9 @@ fn all_dependencies(
681752
(out_packages, out_resolve)
682753
}
683754

684-
fn add_filtered_dependencies(
685-
node: &Node,
686-
config: &SbomConfig,
687-
dep_kinds: &mut DependencyKindMap,
688-
) -> Node {
755+
fn add_filtered_dependencies(node: &Node, config: &SbomConfig) -> Node {
689756
let mut node = node.clone();
690-
node.deps = filtered_dependencies(&node.deps, config, dep_kinds)
691-
.cloned()
692-
.collect();
757+
node.deps = filtered_dependencies(&node.deps, config).cloned().collect();
693758
node.dependencies = node.deps.iter().map(|d| d.pkg.to_owned()).collect();
694759
node
695760
}
@@ -699,18 +764,9 @@ fn add_filtered_dependencies(
699764
fn filtered_dependencies<'a>(
700765
input: &'a [NodeDep],
701766
config: &'a SbomConfig,
702-
dep_kinds: &'a mut DependencyKindMap,
703767
) -> impl Iterator<Item = &'a NodeDep> {
704768
input.iter().filter(|p| {
705769
p.dep_kinds.iter().any(|dep| {
706-
if let Some(deps) = dep_kinds.get_mut(&p.pkg) {
707-
if !deps.iter().any(|d| *d == dep.kind) {
708-
deps.push(dep.kind);
709-
}
710-
} else {
711-
dep_kinds.insert(p.pkg.clone(), vec![dep.kind]);
712-
}
713-
714770
if let Some(true) = config.only_normal_deps {
715771
dep.kind == DependencyKind::Normal
716772
} else {

0 commit comments

Comments
 (0)