1414# Copyright (c) OWASP Foundation. All Rights Reserved.
1515
1616
17- from json import loads as json_loads
18- from typing import Any , Dict , Iterable , List , Optional , Type , Union
17+ from typing import TYPE_CHECKING , Any , Dict , Iterable , List , Optional , Tuple , Type , Union
1918from warnings import warn
2019from xml .etree .ElementTree import Element # nosec B405
2120
2221import serializable
23- from serializable import ObjectMetadataLibrary , ViewType
2422from serializable .helpers import BaseHelper
2523from sortedcontainers import SortedSet
2624
2725from .._internal .compare import ComparableTuple as _ComparableTuple
28- from ..exception .model import MutuallyExclusivePropertiesException
29- from ..model import ExternalReference , HashType , _HashTypeRepositorySerializationHelper
30- from ..model .component import Component
31- from ..model .service import Service
26+ from ..schema import SchemaVersion
3227from ..schema .schema import SchemaVersion1Dot4 , SchemaVersion1Dot5 , SchemaVersion1Dot6
28+ from . import ExternalReference , HashType , _HashTypeRepositorySerializationHelper
29+ from .component import Component
30+ from .service import Service
31+
32+ if TYPE_CHECKING : # pragma: no cover
33+ from serializable import ObjectMetadataLibrary , ViewType
3334
3435
3536@serializable .serializable_class
@@ -164,6 +165,25 @@ def __hash__(self) -> int:
164165 def __repr__ (self ) -> str :
165166 return f'<Tool name={ self .name } , version={ self .version } , vendor={ self .vendor } >'
166167
168+ @classmethod
169+ def from_component (cls : Type ['Tool' ], component : 'Component' ) -> 'Tool' :
170+ return cls (
171+ vendor = component .group ,
172+ name = component .name ,
173+ version = component .version ,
174+ hashes = component .hashes ,
175+ external_references = component .external_references ,
176+ )
177+
178+ @classmethod
179+ def from_service (cls : Type ['Tool' ], service : 'Service' ) -> 'Tool' :
180+ return cls (
181+ vendor = service .group ,
182+ name = service .name ,
183+ version = service .version ,
184+ external_references = service .external_references ,
185+ )
186+
167187
168188class ToolsRepository :
169189 """
@@ -180,7 +200,6 @@ def __init__(
180200 if tools :
181201 warn ('Using Tool is deprecated as of CycloneDX v1.5. Components and Services should be used now. '
182202 'See https://cyclonedx.org/docs/1.5/' , DeprecationWarning )
183-
184203 self ._components = SortedSet (components or [])
185204 self ._services = SortedSet (services or [])
186205 self ._tools = SortedSet (tools or [])
@@ -223,7 +242,9 @@ def __len__(self) -> int:
223242 + len (self ._services )
224243
225244 def __bool__ (self ) -> bool :
226- return any ((self ._tools , self ._components , self ._services ))
245+ return len (self ._tools ) > 0 \
246+ or len (self ._components ) > 0 \
247+ or len (self ._services ) > 0
227248
228249 def __eq__ (self , other : object ) -> bool :
229250 if not isinstance (other , ToolsRepository ):
@@ -238,77 +259,38 @@ def __hash__(self) -> int:
238259
239260
240261class ToolsRepositoryHelper (BaseHelper ):
241- """
242- Helps with serializing and deserializing ToolsRepository objects.
243- """
244262
245- @classmethod
246- def convert_new_to_old (cls , components : Iterable [Component ], services : Iterable [Service ]) -> 'SortedSet[Tool]' :
247- """
248- "Down converts" Component and Service objects to Tools so they can be rendered by
249- the library for schemas less than version 1.5.
250-
251- Returns:
252- A sorted set of Tools
253- """
254- tools_to_render : 'SortedSet[Tool]' = SortedSet ()
255-
256- for c in components :
257- tools_to_render .add (Tool (
258- name = c .name ,
259- vendor = c .group ,
260- version = c .version ,
261- hashes = c .hashes ,
262- external_references = c .external_references ,
263- ))
264-
265- for s in services :
266- if s .provider :
267- vendor = s .provider .name
268- else :
269- vendor = None
270- tools_to_render .add (Tool (
271- name = s .name ,
272- vendor = vendor ,
273- version = s .version ,
274- external_references = s .external_references ,
275- ))
263+ @staticmethod
264+ def __all_as_tools (o : ToolsRepository ) -> Tuple [Tool , ...]:
265+ return (
266+ * o .tools ,
267+ * map (Tool .from_component , o .components ),
268+ * map (Tool .from_service , o .services ),
269+ )
276270
277- return tools_to_render
271+ @staticmethod
272+ def __supports_components_and_services (view : Any ) -> bool :
273+ try :
274+ return view is not None and view ().schema_version_enum >= SchemaVersion .V1_5
275+ except Exception :
276+ return False
278277
279278 @classmethod
280279 def json_normalize (cls , o : ToolsRepository , * ,
281- view : Optional [Type [ViewType ]],
280+ view : Optional [Type [' ViewType' ]],
282281 ** __ : Any ) -> Any :
283- if not any ([ o . tools , o . components , o . services ]) :
282+ if not o :
284283 return None
285-
286- result = {}
287-
288- if (view ().schema_version_enum >= SchemaVersion1Dot5 ().schema_version_enum # type: ignore[union-attr, misc]
289- and not o .tools ):
290- if o .components :
291- result ['components' ] = [json_loads (Component .as_json (c , view_ = view )) # type: ignore[attr-defined]
292- for c in o .components ]
293-
294- if o .services :
295- result ['services' ] = [json_loads (Service .as_json (s , view_ = view )) # type: ignore[attr-defined]
296- for s in o .services ]
297-
298- if result :
299- return result
300-
301- tools_to_render : 'SortedSet[Tool]' = SortedSet (o .tools )
302- # We "down-convert" Components and Services to Tools so we can render to older schemas
303- # or when there are existing Tool objects
304- tools_to_render .update (cls .convert_new_to_old (o .components , o .services ))
305-
306- return [json_loads (Tool .as_json (t , view_ = view )) for t in tools_to_render ] # type: ignore[attr-defined]
284+ return cls .__all_as_tools (o ) \
285+ if len (o .tools ) > 0 or not cls .__supports_components_and_services (view ) \
286+ else {
287+ 'components' : tuple (o .components ) if len (o .components ) > 0 else None ,
288+ 'services' : tuple (o .services ) if len (o .services ) > 0 else None ,
289+ }
307290
308291 @classmethod
309292 def json_denormalize (cls , o : Union [List [Dict [str , Any ]], Dict [str , Any ]],
310293 ** __ : Any ) -> ToolsRepository :
311-
312294 components = []
313295 services = []
314296 tools = []
@@ -331,58 +313,39 @@ def json_denormalize(cls, o: Union[List[Dict[str, Any]], Dict[str, Any]],
331313 @classmethod
332314 def xml_normalize (cls , o : ToolsRepository , * ,
333315 element_name : str ,
334- view : Optional [Type [ViewType ]],
316+ view : Optional [Type [' ViewType' ]],
335317 xmlns : Optional [str ],
336318 ** __ : Any ) -> Optional [Element ]:
337- if not any ([ o . tools , o . components , o . services ]): # pylint: disable=protected-access
319+ if not o :
338320 return None
339-
340321 elem = Element (element_name )
341-
342- if (view ().schema_version_enum >= SchemaVersion1Dot5 ().schema_version_enum # type: ignore[union-attr, misc]
343- and not o .tools ):
322+ if len (o .tools ) > 0 or not cls .__supports_components_and_services (view ):
323+ elem .extend (
324+ ti .as_xml ( # type:ignore[attr-defined]
325+ view_ = view , as_string = False , element_name = 'tool' , xmlns = xmlns )
326+ for ti in cls .__all_as_tools (o )
327+ )
328+ else :
344329 if o .components :
345- c_elem = Element ('{' + xmlns + '}' + 'components' ) # type: ignore[operator]
346-
347- c_elem .extend (
348- c .as_xml ( # type: ignore[attr-defined]
330+ elem_c = Element (f'{{{ xmlns } }}components' if xmlns else 'components' )
331+ elem_c .extend (
332+ ci .as_xml ( # type:ignore[attr-defined]
349333 view_ = view , as_string = False , element_name = 'component' , xmlns = xmlns )
350- for c in o .components
351- )
352-
353- elem .append (c_elem )
354-
334+ for ci in o .components )
335+ elem .append (elem_c )
355336 if o .services :
356- s_elem = Element ('{' + xmlns + '}' + 'services' ) # type: ignore[operator]
357-
358- s_elem .extend (
359- s .as_xml ( # type: ignore[attr-defined]
337+ elem_s = Element (f'{{{ xmlns } }}services' if xmlns else 'services' )
338+ elem_s .extend (
339+ si .as_xml ( # type:ignore[attr-defined]
360340 view_ = view , as_string = False , element_name = 'service' , xmlns = xmlns )
361- for s in o .services
362- )
363-
364- elem .append (s_elem )
365-
366- if len (elem ) > 0 :
367- return elem
368-
369- tools_to_render : 'SortedSet[Tool]' = SortedSet (o .tools )
370- # We "down-convert" Components and Services to Tools so we can render to older schemas
371- # or when there are existing Tool objects
372- tools_to_render .update (cls .convert_new_to_old (o .components , o .services ))
373-
374- elem .extend (
375- t .as_xml ( # type: ignore[attr-defined]
376- view_ = view , as_string = False , element_name = 'tool' , xmlns = xmlns )
377- for t in tools_to_render
378- )
379-
341+ for si in o .services )
342+ elem .append (elem_s )
380343 return elem
381344
382345 @classmethod
383346 def xml_denormalize (cls , o : Element , * ,
384347 default_ns : Optional [str ],
385- prop_info : ObjectMetadataLibrary .SerializableProperty ,
348+ prop_info : ' ObjectMetadataLibrary.SerializableProperty' ,
386349 ctx : Type [Any ],
387350 ** kwargs : Any ) -> ToolsRepository :
388351 tools : List [Tool ] = []
0 commit comments