@@ -12,8 +12,7 @@ use crate::{determine_package_version, formatted_package_name, Package, Parse, U
1212/// Define the generic trait for components.
1313trait Component {
1414 fn component_type ( & self ) -> & str ;
15- fn name ( & self ) -> & str ;
16- fn version ( & self ) -> & str ;
15+ fn version ( & self ) -> Option < & str > ;
1716 fn scope ( & self ) -> Option < & str > ;
1817 fn purl ( & self ) -> Option < & str > ;
1918 fn components ( & self ) -> Option < & [ Self ] >
@@ -40,8 +39,7 @@ struct Components<T> {
4039struct XmlComponent {
4140 #[ serde( rename = "@type" ) ]
4241 component_type : String ,
43- name : String ,
44- version : String ,
42+ version : Option < String > ,
4543 scope : Option < String > ,
4644 purl : Option < String > ,
4745 components : Option < Components < XmlComponent > > ,
@@ -52,12 +50,8 @@ impl Component for XmlComponent {
5250 & self . component_type
5351 }
5452
55- fn name ( & self ) -> & str {
56- & self . name
57- }
58-
59- fn version ( & self ) -> & str {
60- & self . version
53+ fn version ( & self ) -> Option < & str > {
54+ self . version . as_deref ( )
6155 }
6256
6357 fn scope ( & self ) -> Option < & str > {
@@ -78,8 +72,7 @@ impl Component for XmlComponent {
7872struct JsonComponent {
7973 #[ serde( rename = "type" ) ]
8074 component_type : String ,
81- name : String ,
82- version : String ,
75+ version : Option < String > ,
8376 scope : Option < String > ,
8477 purl : Option < String > ,
8578 #[ serde( default ) ]
@@ -91,12 +84,8 @@ impl Component for JsonComponent {
9184 & self . component_type
9285 }
9386
94- fn name ( & self ) -> & str {
95- & self . name
96- }
97-
98- fn version ( & self ) -> & str {
99- & self . version
87+ fn version ( & self ) -> Option < & str > {
88+ self . version . as_deref ( )
10089 }
10190
10291 fn scope ( & self ) -> Option < & str > {
@@ -140,11 +129,12 @@ fn filter_components<T: Component>(components: &[T]) -> impl Iterator<Item = &'_
140129}
141130
142131/// Convert a component's package URL (PURL) into a package object.
143- fn from_purl < T : Component > ( component : & T ) -> anyhow:: Result < Package > {
144- let purl_str = component
145- . purl ( )
146- . ok_or_else ( || anyhow ! ( "Missing purl for {}:{}" , component. name( ) , component. version( ) ) ) ?;
147- let purl = GenericPurl :: < String > :: from_str ( purl_str) ?;
132+ fn from_purl < T : Component > ( component : & T ) -> anyhow:: Result < Option < Package > > {
133+ let purl = match component. purl ( ) {
134+ Some ( purl) => purl,
135+ None => return Ok ( None ) ,
136+ } ;
137+ let purl = GenericPurl :: < String > :: from_str ( purl) ?;
148138 let package_type = PackageType :: from_str ( purl. package_type ( ) ) . map_err ( |_| UnknownEcosystem ) ?;
149139
150140 // Determine the package name based on its type and namespace.
@@ -159,7 +149,7 @@ fn from_purl<T: Component>(component: &T) -> anyhow::Result<Package> {
159149 // Use the qualifiers from the PURL to determine the version details.
160150 let version = determine_package_version ( pkg_version, & purl) ;
161151
162- Ok ( Package { name, version, package_type } )
152+ Ok ( Some ( Package { name, version, package_type } ) )
163153}
164154
165155pub struct CycloneDX ;
@@ -169,6 +159,7 @@ impl CycloneDX {
169159 let comp = components. unwrap_or_default ( ) ;
170160 let packages = filter_components ( comp)
171161 . map ( from_purl)
162+ . flat_map ( Result :: transpose)
172163 . filter ( |r| !r. as_ref ( ) . is_err_and ( |e| e. is :: < UnknownEcosystem > ( ) ) )
173164 . collect :: < anyhow:: Result < Vec < _ > > > ( ) ?;
174165 Ok ( packages)
@@ -278,17 +269,15 @@ mod tests {
278269 fn test_ignore_unsupported_ecosystem ( ) {
279270 let ignored_component = JsonComponent {
280271 component_type : "library" . into ( ) ,
281- name : "adduser" . into ( ) ,
282- version : "3.118ubuntu5" . into ( ) ,
272+ version : Some ( "3.118ubuntu5" . into ( ) ) ,
283273 scope : None ,
284274 purl : Some ( "pkg:deb/ubuntu/[email protected] ?arch=all&distro=ubuntu-22.04" . into ( ) ) , 285275 components : vec ! [ ] ,
286276 } ;
287277
288278 let component = JsonComponent {
289279 component_type : "library" . into ( ) ,
290- name : "abbrev" . into ( ) ,
291- version : "1.1.1" . into ( ) ,
280+ version : Some ( "1.1.1" . into ( ) ) ,
292281 scope : None ,
293282 purl : Some ( "pkg:npm/[email protected] " . into ( ) ) , 294283 components : vec ! [ ] ,
@@ -308,4 +297,37 @@ mod tests {
308297 assert ! ( packages. len( ) == 1 ) ;
309298 assert_eq ! ( packages[ 0 ] , expected_package) ;
310299 }
300+
301+ #[ test]
302+ fn test_ignore_missing_purl ( ) {
303+ let ignored_component = JsonComponent {
304+ component_type : "library" . into ( ) ,
305+ version : Some ( "1.0.0" . into ( ) ) ,
306+ scope : None ,
307+ purl : None ,
308+ components : vec ! [ ] ,
309+ } ;
310+
311+ let component = JsonComponent {
312+ component_type : "library" . into ( ) ,
313+ version : Some ( "2.0.0" . into ( ) ) ,
314+ scope : None ,
315+ purl : Some ( "pkg:npm/[email protected] " . into ( ) ) , 316+ components : vec ! [ ] ,
317+ } ;
318+
319+ let expected_package = Package {
320+ name : "some-package-2" . into ( ) ,
321+ version : PackageVersion :: FirstParty ( "2.0.0" . into ( ) ) ,
322+ package_type : PackageType :: Npm ,
323+ } ;
324+
325+ let bom: Bom < Vec < JsonComponent > > =
326+ Bom { components : Some ( vec ! [ component, ignored_component] ) } ;
327+
328+ let packages = CycloneDX :: process_components ( bom. components . as_deref ( ) ) . unwrap ( ) ;
329+
330+ assert ! ( packages. len( ) == 1 ) ;
331+ assert_eq ! ( packages[ 0 ] , expected_package) ;
332+ }
311333}
0 commit comments