Skip to content

Commit 1a48106

Browse files
committed
refactor: reimplemented XML/JSON normalization
Signed-off-by: Jan Kowalleck <[email protected]>
1 parent 59b0987 commit 1a48106

12 files changed

+112
-165
lines changed

cyclonedx/model/tool.py

Lines changed: 71 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,23 @@
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
1918
from warnings import warn
2019
from xml.etree.ElementTree import Element # nosec B405
2120

2221
import serializable
23-
from serializable import ObjectMetadataLibrary, ViewType
2422
from serializable.helpers import BaseHelper
2523
from sortedcontainers import SortedSet
2624

2725
from .._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
3227
from ..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

168188
class 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

240261
class 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] = []

tests/_data/snapshots/get_bom_with_tools_with_component_and_service_and_tools_migrate-1.2.json.bin

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
"metadata": {
33
"timestamp": "2023-01-07T13:44:32.312678+00:00",
44
"tools": [
5+
{
6+
"name": "test-tool",
7+
"version": "1.33.7"
8+
},
59
{
610
"name": "test-component",
711
"version": "1.2.3"
812
},
913
{
1014
"name": "test-service"
11-
},
12-
{
13-
"name": "test-tool",
14-
"version": "1.33.7"
1515
}
1616
]
1717
},

tests/_data/snapshots/get_bom_with_tools_with_component_and_service_and_tools_migrate-1.2.xml.bin

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
<metadata>
44
<timestamp>2023-01-07T13:44:32.312678+00:00</timestamp>
55
<tools>
6+
<tool>
7+
<name>test-tool</name>
8+
<version>1.33.7</version>
9+
</tool>
610
<tool>
711
<name>test-component</name>
812
<version>1.2.3</version>
913
</tool>
1014
<tool>
1115
<name>test-service</name>
1216
</tool>
13-
<tool>
14-
<name>test-tool</name>
15-
<version>1.33.7</version>
16-
</tool>
1717
</tools>
1818
</metadata>
1919
</bom>

tests/_data/snapshots/get_bom_with_tools_with_component_and_service_and_tools_migrate-1.3.json.bin

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
"metadata": {
33
"timestamp": "2023-01-07T13:44:32.312678+00:00",
44
"tools": [
5+
{
6+
"name": "test-tool",
7+
"version": "1.33.7"
8+
},
59
{
610
"name": "test-component",
711
"version": "1.2.3"
812
},
913
{
1014
"name": "test-service"
11-
},
12-
{
13-
"name": "test-tool",
14-
"version": "1.33.7"
1515
}
1616
]
1717
},

tests/_data/snapshots/get_bom_with_tools_with_component_and_service_and_tools_migrate-1.3.xml.bin

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
<metadata>
44
<timestamp>2023-01-07T13:44:32.312678+00:00</timestamp>
55
<tools>
6+
<tool>
7+
<name>test-tool</name>
8+
<version>1.33.7</version>
9+
</tool>
610
<tool>
711
<name>test-component</name>
812
<version>1.2.3</version>
913
</tool>
1014
<tool>
1115
<name>test-service</name>
1216
</tool>
13-
<tool>
14-
<name>test-tool</name>
15-
<version>1.33.7</version>
16-
</tool>
1717
</tools>
1818
</metadata>
1919
</bom>

tests/_data/snapshots/get_bom_with_tools_with_component_and_service_and_tools_migrate-1.4.json.bin

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
"metadata": {
33
"timestamp": "2023-01-07T13:44:32.312678+00:00",
44
"tools": [
5+
{
6+
"name": "test-tool",
7+
"version": "1.33.7"
8+
},
59
{
610
"name": "test-component",
711
"version": "1.2.3"
812
},
913
{
1014
"name": "test-service"
11-
},
12-
{
13-
"name": "test-tool",
14-
"version": "1.33.7"
1515
}
1616
]
1717
},

tests/_data/snapshots/get_bom_with_tools_with_component_and_service_and_tools_migrate-1.4.xml.bin

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
<metadata>
44
<timestamp>2023-01-07T13:44:32.312678+00:00</timestamp>
55
<tools>
6+
<tool>
7+
<name>test-tool</name>
8+
<version>1.33.7</version>
9+
</tool>
610
<tool>
711
<name>test-component</name>
812
<version>1.2.3</version>
913
</tool>
1014
<tool>
1115
<name>test-service</name>
1216
</tool>
13-
<tool>
14-
<name>test-tool</name>
15-
<version>1.33.7</version>
16-
</tool>
1717
</tools>
1818
</metadata>
1919
</bom>

0 commit comments

Comments
 (0)