@@ -74,11 +74,6 @@ use validator::ValidateEmail;
7474type PackageMap = BTreeMap < PackageId , Package > ;
7575type ResolveMap = BTreeMap < PackageId , Node > ;
7676type DependencyKindMap = BTreeMap < PackageId , DependencyKind > ;
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 ) > ;
8277
8378/// The values are ordered from weakest to strongest so that casting to integer would make sense
8479#[ derive( Debug , PartialEq , Eq , PartialOrd , Ord , Copy , Clone , Hash ) ]
@@ -131,21 +126,19 @@ impl SbomGenerator {
131126 log:: trace!( "Processing the workspace {}" , meta. workspace_root) ;
132127 let members: Vec < PackageId > = meta. workspace_members ;
133128 let packages = index_packages ( meta. packages ) ;
134- let resolve = index_resolve ( meta. resolve . unwrap ( ) . nodes ) ;
135-
136- let built = index_built_edges ( & packages, & resolve) ;
129+ let resolve = prune_unbuilt_edges ( index_resolve ( meta. resolve . unwrap ( ) . nodes ) , & packages) ;
137130
138131 let mut result = Vec :: with_capacity ( members. len ( ) ) ;
139132 for member in members. iter ( ) {
140133 log:: trace!( "Processing the package {}" , member) ;
141134
142- let dep_kinds = index_dep_kinds ( member, & resolve, & built ) ;
135+ let dep_kinds = index_dep_kinds ( member, & resolve) ;
143136
144137 let ( dependencies, pruned_resolve) =
145138 if config. included_dependencies ( ) == IncludedDependencies :: AllDependencies {
146- all_dependencies ( member, & packages, & resolve, & built , config)
139+ all_dependencies ( member, & packages, & resolve, config)
147140 } else {
148- top_level_dependencies ( member, & packages, & resolve, & built , config)
141+ top_level_dependencies ( member, & packages, & resolve, config)
149142 } ;
150143
151144 let manifest_path = packages[ member] . manifest_path . clone ( ) . into_std_path_buf ( ) ;
@@ -636,11 +629,7 @@ fn index_resolve(packages: Vec<Node>) -> ResolveMap {
636629 . collect ( )
637630}
638631
639- fn index_dep_kinds (
640- root : & PackageId ,
641- resolve : & ResolveMap ,
642- built : & BuiltEdges ,
643- ) -> DependencyKindMap {
632+ fn index_dep_kinds ( root : & PackageId , resolve : & ResolveMap ) -> DependencyKindMap {
644633 // cache strongest found dependency kind for every node
645634 let mut id_to_dep_kind: HashMap < PackageId , PrivateDepKind > = HashMap :: new ( ) ;
646635 id_to_dep_kind. insert ( root. clone ( ) , PrivateDepKind :: Runtime ) ;
@@ -674,14 +663,6 @@ fn index_dep_kinds(
674663 let node = & resolve[ & pkg_id] ;
675664 for child_dep in & node. deps {
676665 for dep_kind in & child_dep. dep_kinds {
677- // Unlike `filtered_dependencies` this walk deliberately keeps
678- // dev- and build-dependencies, since their whole purpose here is
679- // to mark components as `Excluded`. Optional dependencies that
680- // were never activated are still skipped: an edge that is not
681- // built must not raise the scope of a package reached elsewhere.
682- if !built. contains ( & ( & pkg_id, & child_dep. pkg , dep_kind. kind ) ) {
683- continue ;
684- }
685666 let current_kind = PrivateDepKind :: from ( & dep_kind. kind ) ;
686667 let new_path_node_kind = min ( current_kind, path_node_kind) ;
687668
@@ -741,13 +722,12 @@ fn top_level_dependencies(
741722 root : & PackageId ,
742723 packages : & PackageMap ,
743724 resolve : & ResolveMap ,
744- built : & BuiltEdges ,
745725 config : & SbomConfig ,
746726) -> ( PackageMap , ResolveMap ) {
747727 log:: trace!( "Adding top-level dependencies to SBOM" ) ;
748728
749729 // Only include packages that have dependency kinds other than "Development"
750- let root_node = add_filtered_dependencies ( & resolve[ root] , built , config) ;
730+ let root_node = add_filtered_dependencies ( & resolve[ root] , config) ;
751731
752732 let mut pkg_result = PackageMap :: new ( ) ;
753733
@@ -775,7 +755,6 @@ fn all_dependencies(
775755 root : & PackageId ,
776756 packages : & PackageMap ,
777757 resolve : & ResolveMap ,
778- built : & BuiltEdges ,
779758 config : & SbomConfig ,
780759) -> ( PackageMap , ResolveMap ) {
781760 log:: trace!( "Adding all dependencies to SBOM" ) ;
@@ -797,13 +776,10 @@ fn all_dependencies(
797776 // If we haven't processed this node yet...
798777 if !out_resolve. contains_key ( & node. id ) {
799778 // Add the node to the output
800- out_resolve. insert (
801- node. id . to_owned ( ) ,
802- add_filtered_dependencies ( node, built, config) ,
803- ) ;
779+ out_resolve. insert ( node. id . to_owned ( ) , add_filtered_dependencies ( node, config) ) ;
804780 // Queue its dependencies for the next BFS loop iteration
805781 next_queue. extend (
806- filtered_dependencies ( node, built , config) . map ( |dep| & resolve[ & dep. pkg ] ) ,
782+ filtered_dependencies ( & node. deps , config) . map ( |dep| & resolve[ & dep. pkg ] ) ,
807783 ) ;
808784 }
809785 }
@@ -820,91 +796,72 @@ fn all_dependencies(
820796 ( out_packages, out_resolve)
821797}
822798
823- fn add_filtered_dependencies ( node : & Node , built : & BuiltEdges , config : & SbomConfig ) -> Node {
824- let mut node_copy = node. clone ( ) ;
825- node_copy. deps = filtered_dependencies ( node, built, config)
826- . cloned ( )
827- . collect ( ) ;
828- node_copy. dependencies = node_copy. deps . iter ( ) . map ( |d| d. pkg . to_owned ( ) ) . collect ( ) ;
829- node_copy
799+ fn add_filtered_dependencies ( node : & Node , config : & SbomConfig ) -> Node {
800+ let mut node = node. clone ( ) ;
801+ node. deps = filtered_dependencies ( & node. deps , config) . cloned ( ) . collect ( ) ;
802+ node. dependencies = node. deps . iter ( ) . map ( |d| d. pkg . to_owned ( ) ) . collect ( ) ;
803+ node
830804}
831805
832- /// Filters out the dependencies of `node` that do not end up being built:
833- ///
834- /// * dependencies only used for development, specified under `[dev-dependencies]`
835- /// in `Cargo.toml` (and also build dependencies, if so configured);
836- /// * optional dependencies that none of the enabled features activate.
837- ///
838- /// `cargo metadata` reports optional dependencies in `resolve.nodes[].deps`
839- /// whether or not the resolver enabled them, so the second group has to be
840- /// recovered by replaying the feature resolution recorded in
841- /// `resolve.nodes[].features`. See
842- /// <https://github.com/CycloneDX/cyclonedx-rust-cargo/issues/766>.
806+ /// Filters out dependencies only used for development, and not affecting the final binary.
807+ /// These are specified under `[dev-dependencies]` in Cargo.toml.
843808fn filtered_dependencies < ' a > (
844- node : & ' a Node ,
845- built : & ' a BuiltEdges < ' a > ,
809+ input : & ' a [ NodeDep ] ,
846810 config : & ' a SbomConfig ,
847811) -> impl Iterator < Item = & ' a NodeDep > {
848- node. deps . iter ( ) . filter ( move |edge| {
849- edge. dep_kinds . iter ( ) . any ( |dep_kind| {
850- included_kind ( dep_kind. kind , config)
851- && built. contains ( & ( & node. id , & edge. pkg , dep_kind. kind ) )
812+ input. iter ( ) . filter ( |p| {
813+ p. dep_kinds . iter ( ) . any ( |dep| {
814+ if let Some ( true ) = config. only_normal_deps {
815+ dep. kind == DependencyKind :: Normal
816+ } else {
817+ dep. kind != DependencyKind :: Development
818+ }
852819 } )
853820 } )
854821}
855822
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
885- }
823+ /// Removes the resolve edges cargo does not actually build: optional dependencies
824+ /// that no enabled feature activates.
825+ ///
826+ /// `cargo metadata` reports those in `resolve.nodes[].deps` whether or not the
827+ /// resolver enabled them, so they have to be recovered by replaying the feature
828+ /// resolution recorded alongside them in `resolve.nodes[].features`. See
829+ /// <https://github.com/CycloneDX/cyclonedx-rust-cargo/issues/766>.
830+ ///
831+ /// Every lookup fails open: an edge that cannot be matched to a manifest entry is
832+ /// kept, so this can remove false positives but not introduce false negatives.
833+ fn prune_unbuilt_edges ( mut resolve : ResolveMap , packages : & PackageMap ) -> ResolveMap {
834+ for ( parent_id, node) in resolve. iter_mut ( ) {
835+ // Without the parent's manifest there is nothing to check against.
836+ let Some ( parent) = packages. get ( parent_id) else {
837+ continue ;
838+ } ;
839+ let activated = activated_dependencies ( & parent. features , & node. features ) ;
886840
887- /// Whether a dependency of this kind belongs in the SBOM at all.
888- fn included_kind ( kind : DependencyKind , config : & SbomConfig ) -> bool {
889- if let Some ( true ) = config. only_normal_deps {
890- kind == DependencyKind :: Normal
891- } else {
892- kind != DependencyKind :: Development
841+ node. deps . retain_mut ( |edge| {
842+ let Some ( child) = packages. get ( & edge. pkg ) else {
843+ return true ;
844+ } ;
845+ edge. dep_kinds
846+ . retain ( |dep_kind| is_built ( parent, child, dep_kind, & activated) ) ;
847+ !edge. dep_kinds . is_empty ( )
848+ } ) ;
849+ node. dependencies = node. deps . iter ( ) . map ( |edge| edge. pkg . clone ( ) ) . collect ( ) ;
893850 }
851+
852+ resolve
894853}
895854
896855/// Whether `parent` actually builds `child` as a dependency of the given kind and
897856/// platform, or whether it is an optional dependency that no enabled feature
898857/// activates.
899858///
900- /// A single edge in the resolve graph can be backed by more than one entry in the
901- /// parent's manifest - the same crate can be depended on twice under different
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.
859+ /// One resolve edge can be backed by several entries in the parent's manifest -
860+ /// the same crate can be depended on twice under different renames - so the edge
861+ /// survives if any entry that could have produced it is non-optional or activated.
904862fn is_built (
905863 parent : & Package ,
906864 child : & Package ,
907- edge_name : & str ,
908865 dep_kind : & DepKindInfo ,
909866 activated : & HashSet < & str > ,
910867) -> bool {
@@ -922,50 +879,32 @@ fn is_built(
922879 return true ;
923880 }
924881
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
882+ // Pre-release versions and `[patch]` can defeat the version requirement, so a
883+ // narrowing that would leave nothing to choose from is skipped.
884+ let matching: Vec < & CargoDependency > = candidates
935885 . 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 ( ) {
886+ . copied ( )
887+ . filter ( |dep| dep. req . matches ( & child. version ) )
888+ . collect ( ) ;
889+ let candidates = if matching. is_empty ( ) {
944890 candidates
945891 } else {
946- narrowed
947- }
948- }
892+ matching
893+ } ;
949894
950- /// The name a dependency is known by in feature syntax: the rename if it was
951- /// renamed with `package = "..."`, and the package name otherwise.
952- fn dependency_key ( dep : & CargoDependency ) -> & str {
953- dep. rename . as_deref ( ) . unwrap_or ( & dep. name )
895+ // Feature syntax refers to a renamed dependency by its rename.
896+ candidates
897+ . iter ( )
898+ . any ( | dep| !dep . optional || activated . contains ( dep . rename . as_deref ( ) . unwrap_or ( & dep. name ) ) )
954899}
955900
956901/// Collects the optional dependencies that `enabled_features` activate, given a
957902/// package's `[features]` table as reported by `cargo metadata`.
958903///
959- /// Every entry of a feature is one of four things:
960- ///
961- /// * `dep:foo` - activates the optional dependency `foo`;
962- /// * `foo/bar` - activates `foo` if it is optional, and enables `bar` on it;
963- /// * `foo?/bar` - enables `bar` on `foo`, but only if something else activates it;
964- /// * `bar` - another feature of this package, expanded recursively.
965- ///
966- /// Optional dependencies that are never named with `dep:` also get an implicit
967- /// feature of their own, but `cargo metadata` normalizes those into an explicit
968- /// `"foo": ["dep:foo"]` entry, so they need no special handling here.
904+ /// `dep:foo` and `foo/bar` activate `foo`, the weak `foo?/bar` does not, and
905+ /// anything else is another feature of this package, expanded recursively.
906+ /// Optional dependencies with an implicit feature of their own arrive normalized
907+ /// as `"foo": ["dep:foo"]`, so they need no special handling.
969908fn activated_dependencies < ' a > (
970909 features : & ' a BTreeMap < String , Vec < String > > ,
971910 enabled_features : & ' a [ String ] ,
@@ -1328,15 +1267,6 @@ mod test {
13281267 assert_eq ! ( activated( & features, & [ ] ) , set( & [ ] ) ) ;
13291268 }
13301269
1331- #[ test]
1332- fn implicit_features_activate_an_optional_dependency ( ) {
1333- // An optional dependency that no feature mentions with `dep:` gets a
1334- // feature of its own, which `cargo metadata` reports in this normalized form.
1335- let features = features ( & [ ( "serde_json" , & [ "dep:serde_json" ] ) ] ) ;
1336-
1337- assert_eq ! ( activated( & features, & [ "serde_json" ] ) , set( & [ "serde_json" ] ) ) ;
1338- }
1339-
13401270 #[ test]
13411271 fn features_are_expanded_transitively ( ) {
13421272 let features = features ( & [
@@ -1358,28 +1288,13 @@ mod test {
13581288 assert_eq ! ( activated( & features, & [ "strong" , "weak" ] ) , set( & [ "chrono" ] ) ) ;
13591289 }
13601290
1361- #[ test]
1362- fn dependencies_are_keyed_on_the_rename ( ) {
1363- // `chrono_0_4 = { package = "chrono", optional = true }` is activated as
1364- // `chrono_0_4`, never as `chrono`.
1365- let features = features ( & [ ( "dates" , & [ "dep:chrono_0_4" ] ) ] ) ;
1366-
1367- assert_eq ! ( activated( & features, & [ "dates" ] ) , set( & [ "chrono_0_4" ] ) ) ;
1368- }
1369-
13701291 #[ test]
13711292 fn cyclic_features_terminate ( ) {
13721293 let features = features ( & [ ( "a" , & [ "b" , "dep:x" ] ) , ( "b" , & [ "a" ] ) ] ) ;
13731294
13741295 assert_eq ! ( activated( & features, & [ "a" ] ) , set( & [ "x" ] ) ) ;
13751296 }
13761297
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-
13831298 #[ test]
13841299 fn unknown_features_are_ignored ( ) {
13851300 assert_eq ! ( activated( & features( & [ ] ) , & [ "not-a-feature" ] ) , set( & [ ] ) ) ;
0 commit comments