@@ -74,10 +74,11 @@ use validator::ValidateEmail;
7474type PackageMap = BTreeMap < PackageId , Package > ;
7575type ResolveMap = BTreeMap < PackageId , Node > ;
7676type DependencyKindMap = BTreeMap < PackageId , DependencyKind > ;
77- /// Which optional dependencies each package's enabled features activate,
78- /// keyed on the name feature syntax refers to them by. Worked out once per
79- /// `cargo metadata` invocation because it does not vary by workspace member.
80- type ActivationMap < ' a > = HashMap < & ' a PackageId , HashSet < & ' a str > > ;
77+ /// The edges of the resolve graph that cargo actually builds, as
78+ /// `(parent, child, kind)`. Optional dependencies that no enabled feature
79+ /// activates are absent. Worked out once per `cargo metadata` invocation
80+ /// because it does not vary by workspace member.
81+ type BuiltEdges < ' a > = HashSet < ( & ' a PackageId , & ' a PackageId , DependencyKind ) > ;
8182
8283/// The values are ordered from weakest to strongest so that casting to integer would make sense
8384#[ derive( Debug , PartialEq , Eq , PartialOrd , Ord , Copy , Clone , Hash ) ]
@@ -132,19 +133,19 @@ impl SbomGenerator {
132133 let packages = index_packages ( meta. packages ) ;
133134 let resolve = index_resolve ( meta. resolve . unwrap ( ) . nodes ) ;
134135
135- let activated = index_activated_dependencies ( & packages, & resolve) ;
136+ let built = index_built_edges ( & packages, & resolve) ;
136137
137138 let mut result = Vec :: with_capacity ( members. len ( ) ) ;
138139 for member in members. iter ( ) {
139140 log:: trace!( "Processing the package {}" , member) ;
140141
141- let dep_kinds = index_dep_kinds ( member, & packages , & resolve, & activated ) ;
142+ let dep_kinds = index_dep_kinds ( member, & resolve, & built ) ;
142143
143144 let ( dependencies, pruned_resolve) =
144145 if config. included_dependencies ( ) == IncludedDependencies :: AllDependencies {
145- all_dependencies ( member, & packages, & resolve, & activated , config)
146+ all_dependencies ( member, & packages, & resolve, & built , config)
146147 } else {
147- top_level_dependencies ( member, & packages, & resolve, & activated , config)
148+ top_level_dependencies ( member, & packages, & resolve, & built , config)
148149 } ;
149150
150151 let manifest_path = packages[ member] . manifest_path . clone ( ) . into_std_path_buf ( ) ;
@@ -637,9 +638,8 @@ fn index_resolve(packages: Vec<Node>) -> ResolveMap {
637638
638639fn index_dep_kinds (
639640 root : & PackageId ,
640- packages : & PackageMap ,
641641 resolve : & ResolveMap ,
642- activated : & ActivationMap ,
642+ built : & BuiltEdges ,
643643) -> DependencyKindMap {
644644 // cache strongest found dependency kind for every node
645645 let mut id_to_dep_kind: HashMap < PackageId , PrivateDepKind > = HashMap :: new ( ) ;
@@ -672,19 +672,15 @@ fn index_dep_kinds(
672672 }
673673
674674 let node = & resolve[ & pkg_id] ;
675- let parent = packages. get ( & pkg_id) . zip ( activated. get ( & pkg_id) ) ;
676675 for child_dep in & node. deps {
677- let child = packages. get ( & child_dep. pkg ) ;
678676 for dep_kind in & child_dep. dep_kinds {
679677 // Unlike `filtered_dependencies` this walk deliberately keeps
680678 // dev- and build-dependencies, since their whole purpose here is
681679 // to mark components as `Excluded`. Optional dependencies that
682680 // were never activated are still skipped: an edge that is not
683681 // built must not raise the scope of a package reached elsewhere.
684- if let ( Some ( ( parent, activated) ) , Some ( child) ) = ( parent, child) {
685- if !is_activated ( parent, child, dep_kind, activated) {
686- continue ;
687- }
682+ if !built. contains ( & ( & pkg_id, & child_dep. pkg , dep_kind. kind ) ) {
683+ continue ;
688684 }
689685 let current_kind = PrivateDepKind :: from ( & dep_kind. kind ) ;
690686 let new_path_node_kind = min ( current_kind, path_node_kind) ;
@@ -745,13 +741,13 @@ fn top_level_dependencies(
745741 root : & PackageId ,
746742 packages : & PackageMap ,
747743 resolve : & ResolveMap ,
748- activated : & ActivationMap ,
744+ built : & BuiltEdges ,
749745 config : & SbomConfig ,
750746) -> ( PackageMap , ResolveMap ) {
751747 log:: trace!( "Adding top-level dependencies to SBOM" ) ;
752748
753749 // Only include packages that have dependency kinds other than "Development"
754- let root_node = add_filtered_dependencies ( & resolve[ root] , packages , activated , config) ;
750+ let root_node = add_filtered_dependencies ( & resolve[ root] , built , config) ;
755751
756752 let mut pkg_result = PackageMap :: new ( ) ;
757753
@@ -779,7 +775,7 @@ fn all_dependencies(
779775 root : & PackageId ,
780776 packages : & PackageMap ,
781777 resolve : & ResolveMap ,
782- activated : & ActivationMap ,
778+ built : & BuiltEdges ,
783779 config : & SbomConfig ,
784780) -> ( PackageMap , ResolveMap ) {
785781 log:: trace!( "Adding all dependencies to SBOM" ) ;
@@ -803,12 +799,11 @@ fn all_dependencies(
803799 // Add the node to the output
804800 out_resolve. insert (
805801 node. id . to_owned ( ) ,
806- add_filtered_dependencies ( node, packages , activated , config) ,
802+ add_filtered_dependencies ( node, built , config) ,
807803 ) ;
808804 // Queue its dependencies for the next BFS loop iteration
809805 next_queue. extend (
810- filtered_dependencies ( node, packages, activated, config)
811- . map ( |dep| & resolve[ & dep. pkg ] ) ,
806+ filtered_dependencies ( node, built, config) . map ( |dep| & resolve[ & dep. pkg ] ) ,
812807 ) ;
813808 }
814809 }
@@ -825,14 +820,9 @@ fn all_dependencies(
825820 ( out_packages, out_resolve)
826821}
827822
828- fn add_filtered_dependencies (
829- node : & Node ,
830- packages : & PackageMap ,
831- activated : & ActivationMap ,
832- config : & SbomConfig ,
833- ) -> Node {
823+ fn add_filtered_dependencies ( node : & Node , built : & BuiltEdges , config : & SbomConfig ) -> Node {
834824 let mut node_copy = node. clone ( ) ;
835- node_copy. deps = filtered_dependencies ( node, packages , activated , config)
825+ node_copy. deps = filtered_dependencies ( node, built , config)
836826 . cloned ( )
837827 . collect ( ) ;
838828 node_copy. dependencies = node_copy. deps . iter ( ) . map ( |d| d. pkg . to_owned ( ) ) . collect ( ) ;
@@ -852,43 +842,46 @@ fn add_filtered_dependencies(
852842/// <https://github.com/CycloneDX/cyclonedx-rust-cargo/issues/766>.
853843fn filtered_dependencies < ' a > (
854844 node : & ' a Node ,
855- packages : & ' a PackageMap ,
856- activated : & ' a ActivationMap ,
845+ built : & ' a BuiltEdges < ' a > ,
857846 config : & ' a SbomConfig ,
858847) -> impl Iterator < Item = & ' a NodeDep > {
859- let parent = packages. get ( & node. id ) . zip ( activated. get ( & node. id ) ) ;
860-
861848 node. deps . iter ( ) . filter ( move |edge| {
862849 edge. dep_kinds . iter ( ) . any ( |dep_kind| {
863850 included_kind ( dep_kind. kind , config)
864- && match ( parent, packages. get ( & edge. pkg ) ) {
865- ( Some ( ( parent, activated) ) , Some ( child) ) => {
866- is_activated ( parent, child, dep_kind, activated)
867- }
868- // Without both manifests there is nothing to check against,
869- // so keep the edge rather than risk dropping a real dependency.
870- _ => true ,
871- }
851+ && built. contains ( & ( & node. id , & edge. pkg , dep_kind. kind ) )
872852 } )
873853 } )
874854}
875855
876- /// Works out, for every package in the resolve graph, which of its optional
877- /// dependencies its enabled features activate.
878- fn index_activated_dependencies < ' a > (
879- packages : & ' a PackageMap ,
880- resolve : & ' a ResolveMap ,
881- ) -> ActivationMap < ' a > {
882- resolve
883- . iter ( )
884- . filter_map ( |( id, node) | {
885- let package = packages. get ( id) ?;
886- Some ( (
887- id,
888- activated_dependencies ( & package. features , & node. features ) ,
889- ) )
890- } )
891- . collect ( )
856+ /// Works out which edges of the resolve graph cargo actually builds, so that the
857+ /// optional dependencies no enabled feature activates can be left out.
858+ fn index_built_edges < ' a > ( packages : & ' a PackageMap , resolve : & ' a ResolveMap ) -> BuiltEdges < ' a > {
859+ let mut built = BuiltEdges :: new ( ) ;
860+
861+ for ( parent_id, node) in resolve {
862+ // Without the parent's manifest there is nothing to check against, so keep
863+ // all of its edges rather than risk dropping a real dependency.
864+ let parent = packages. get ( parent_id) ;
865+ let activated = parent
866+ . map ( |parent| activated_dependencies ( & parent. features , & node. features ) )
867+ . unwrap_or_default ( ) ;
868+
869+ for edge in & node. deps {
870+ for dep_kind in & edge. dep_kinds {
871+ let keep = match ( parent, packages. get ( & edge. pkg ) ) {
872+ ( Some ( parent) , Some ( child) ) => {
873+ is_built ( parent, child, & edge. name , dep_kind, & activated)
874+ }
875+ _ => true ,
876+ } ;
877+ if keep {
878+ built. insert ( ( parent_id, & edge. pkg , dep_kind. kind ) ) ;
879+ }
880+ }
881+ }
882+ }
883+
884+ built
892885}
893886
894887/// Whether a dependency of this kind belongs in the SBOM at all.
@@ -900,41 +893,58 @@ fn included_kind(kind: DependencyKind, config: &SbomConfig) -> bool {
900893 }
901894}
902895
903- /// Whether `parent` actually builds `child` as a dependency of the given kind and platform.
896+ /// Whether `parent` actually builds `child` as a dependency of the given kind and
897+ /// platform, or whether it is an optional dependency that no enabled feature
898+ /// activates.
904899///
905900/// A single edge in the resolve graph can be backed by more than one entry in the
906901/// parent's manifest - the same crate can be depended on twice under different
907- /// renames - so the edge survives if any of those entries is non-optional or is
908- /// activated by an enabled feature .
909- fn is_activated (
902+ /// renames - so the candidates are narrowed down to the entry that produced this
903+ /// edge, and the edge survives if what is left is non-optional or activated .
904+ fn is_built (
910905 parent : & Package ,
911906 child : & Package ,
907+ edge_name : & str ,
912908 dep_kind : & DepKindInfo ,
913909 activated : & HashSet < & str > ,
914910) -> bool {
915- let declares_edge = |dep : & & CargoDependency | {
916- dep. name == child. name && dep. kind == dep_kind. kind && dep. target == dep_kind. target
917- } ;
918- let is_built =
919- |dep : & & CargoDependency | !dep. optional || activated. contains ( dependency_key ( dep) ) ;
920-
921- // The version requirement tells same-named entries apart.
922- let mut by_version = parent
911+ let candidates: Vec < & CargoDependency > = parent
923912 . dependencies
924913 . iter ( )
925- . filter ( declares_edge)
926- . filter ( |dep| dep. req . matches ( & child. version ) )
927- . peekable ( ) ;
928- if by_version. peek ( ) . is_some ( ) {
929- return by_version. any ( |dep| is_built ( & dep) ) ;
914+ . filter ( |dep| {
915+ dep. name == child. name && dep. kind == dep_kind. kind && dep. target == dep_kind. target
916+ } )
917+ . collect ( ) ;
918+
919+ // An edge with no matching manifest entry at all is not something we
920+ // understand, so keep it.
921+ if candidates. is_empty ( ) {
922+ return true ;
930923 }
931924
932- // If the version requirement ruled out every entry - pre-release versions and
933- // `[patch]` can both do that - fall back to matching on the name alone rather
934- // than dropping the edge. An edge with no matching manifest entry at all is
935- // not something we understand, so keep that too.
936- let mut by_name = parent. dependencies . iter ( ) . filter ( declares_edge) . peekable ( ) ;
937- by_name. peek ( ) . is_none ( ) || by_name. any ( |dep| is_built ( & dep) )
925+ // Either narrowing can rule out every candidate - pre-release versions and
926+ // `[patch]` defeat the version requirement, and a `[lib] name` that differs
927+ // from the package name defeats the edge name - so a narrowing that would
928+ // leave nothing to choose from is skipped.
929+ let candidates = narrow ( candidates, |dep| dep. req . matches ( & child. version ) ) ;
930+ let candidates = narrow ( candidates, |dep| {
931+ dependency_key ( dep) . replace ( '-' , "_" ) == edge_name
932+ } ) ;
933+
934+ candidates
935+ . iter ( )
936+ . any ( |dep| !dep. optional || activated. contains ( dependency_key ( dep) ) )
937+ }
938+
939+ /// Keeps the candidates matching `predicate`, or all of them if that would leave
940+ /// none.
941+ fn narrow < T : Copy > ( candidates : Vec < T > , predicate : impl Fn ( & T ) -> bool ) -> Vec < T > {
942+ let narrowed: Vec < T > = candidates. iter ( ) . copied ( ) . filter ( & predicate) . collect ( ) ;
943+ if narrowed. is_empty ( ) {
944+ candidates
945+ } else {
946+ narrowed
947+ }
938948}
939949
940950/// The name a dependency is known by in feature syntax: the rename if it was
@@ -1364,6 +1374,12 @@ mod test {
13641374 assert_eq ! ( activated( & features, & [ "a" ] ) , set( & [ "x" ] ) ) ;
13651375 }
13661376
1377+ #[ test]
1378+ fn narrowing_that_leaves_nothing_is_skipped ( ) {
1379+ assert_eq ! ( narrow( vec![ 1 , 2 , 3 ] , |n| * n > 1 ) , vec![ 2 , 3 ] ) ;
1380+ assert_eq ! ( narrow( vec![ 1 , 2 , 3 ] , |n| * n > 9 ) , vec![ 1 , 2 , 3 ] ) ;
1381+ }
1382+
13671383 #[ test]
13681384 fn unknown_features_are_ignored ( ) {
13691385 assert_eq ! ( activated( & features( & [ ] ) , & [ "not-a-feature" ] ) , set( & [ ] ) ) ;
0 commit comments