11use 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;
5455use regex:: Regex ;
5556
5657use log:: Level ;
57- use std:: collections:: BTreeMap ;
5858use std:: collections:: HashMap ;
59+ use std:: collections:: { BTreeMap , LinkedList } ;
5960use std:: convert:: TryFrom ;
6061use std:: fs:: File ;
6162use 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
6970type PackageMap = BTreeMap < PackageId , Package > ;
7071type 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
73109pub 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 ) ]
563640pub 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(
699764fn 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