diff --git a/cyclonedx/model/bom.py b/cyclonedx/model/bom.py index 7b657604..f1768466 100644 --- a/cyclonedx/model/bom.py +++ b/cyclonedx/model/bom.py @@ -36,14 +36,14 @@ SchemaVersion1Dot5, SchemaVersion1Dot6, ) -from ..serialization import LicenseRepositoryHelper, UrnUuidHelper +from ..serialization import UrnUuidHelper from . import _BOM_LINK_PREFIX, ExternalReference, Property from .bom_ref import BomRef from .component import Component from .contact import OrganizationalContact, OrganizationalEntity from .definition import Definitions from .dependency import Dependable, Dependency -from .license import License, LicenseExpression, LicenseRepository +from .license import License, LicenseExpression, LicenseRepository, _LicenseRepositorySerializationHelper from .lifecycle import Lifecycle, LifecycleRepository, _LifecycleRepositoryHelper from .service import Service from .tool import Tool, ToolRepository, _ToolRepositoryHelper @@ -254,7 +254,7 @@ def supplier(self, supplier: Optional[OrganizationalEntity]) -> None: @serializable.view(SchemaVersion1Dot4) @serializable.view(SchemaVersion1Dot5) @serializable.view(SchemaVersion1Dot6) - @serializable.type_mapping(LicenseRepositoryHelper) + @serializable.type_mapping(_LicenseRepositorySerializationHelper) @serializable.xml_sequence(9) def licenses(self) -> LicenseRepository: """ diff --git a/cyclonedx/model/bom_ref.py b/cyclonedx/model/bom_ref.py index faf47cf4..85bcf501 100644 --- a/cyclonedx/model/bom_ref.py +++ b/cyclonedx/model/bom_ref.py @@ -16,10 +16,20 @@ # Copyright (c) OWASP Foundation. All Rights Reserved. -from typing import Any, Optional +from typing import TYPE_CHECKING, Any, Optional +import serializable -class BomRef: +from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException + +if TYPE_CHECKING: # pragma: no cover + from typing import Type, TypeVar + + _T_BR = TypeVar('_T_BR', bound='BomRef') + + +@serializable.serializable_class +class BomRef(serializable.helpers.BaseHelper): """ An identifier that can be used to reference objects elsewhere in the BOM. @@ -33,6 +43,8 @@ def __init__(self, value: Optional[str] = None) -> None: self.value = value @property + @serializable.json_name('.') + @serializable.xml_name('.') def value(self) -> Optional[str]: return self._value @@ -67,3 +79,23 @@ def __str__(self) -> str: def __bool__(self) -> bool: return self._value is not None + + # region impl BaseHelper + + @classmethod + def serialize(cls, o: Any) -> Optional[str]: + if isinstance(o, cls): + return o.value + raise SerializationOfUnexpectedValueException( + f'Attempt to serialize a non-BomRef: {o!r}') + + @classmethod + def deserialize(cls: 'Type[_T_BR]', o: Any) -> '_T_BR': + try: + return cls(value=str(o)) + except ValueError as err: + raise CycloneDxDeserializationException( + f'BomRef string supplied does not parse: {o!r}' + ) from err + + # endregion impl BaseHelper diff --git a/cyclonedx/model/component.py b/cyclonedx/model/component.py index f553d5c0..8b36e498 100644 --- a/cyclonedx/model/component.py +++ b/cyclonedx/model/component.py @@ -44,7 +44,7 @@ SchemaVersion1Dot5, SchemaVersion1Dot6, ) -from ..serialization import BomRefHelper, LicenseRepositoryHelper, PackageUrl as PackageUrlSH +from ..serialization import PackageUrl as PackageUrlSH from . import ( AttachedText, Copyright, @@ -61,7 +61,7 @@ from .crypto import CryptoProperties from .dependency import Dependable from .issue import IssueType -from .license import License, LicenseRepository +from .license import License, LicenseRepository, _LicenseRepositorySerializationHelper from .release_note import ReleaseNotes @@ -250,7 +250,7 @@ def __init__( # ... # TODO since CDX1.5 @property - @serializable.type_mapping(LicenseRepositoryHelper) + @serializable.type_mapping(_LicenseRepositorySerializationHelper) @serializable.xml_sequence(4) def licenses(self) -> LicenseRepository: """ @@ -1171,7 +1171,7 @@ def mime_type(self, mime_type: Optional[str]) -> None: @property @serializable.json_name('bom-ref') - @serializable.type_mapping(BomRefHelper) + @serializable.type_mapping(BomRef) @serializable.view(SchemaVersion1Dot1) @serializable.view(SchemaVersion1Dot2) @serializable.view(SchemaVersion1Dot3) @@ -1407,7 +1407,7 @@ def hashes(self, hashes: Iterable[HashType]) -> None: @serializable.view(SchemaVersion1Dot4) @serializable.view(SchemaVersion1Dot5) @serializable.view(SchemaVersion1Dot6) - @serializable.type_mapping(LicenseRepositoryHelper) + @serializable.type_mapping(_LicenseRepositorySerializationHelper) @serializable.xml_sequence(12) def licenses(self) -> LicenseRepository: """ @@ -1789,4 +1789,4 @@ def __hash__(self) -> int: def __repr__(self) -> str: return f'' + f'version={self.version}, type={self.type}>' diff --git a/cyclonedx/model/contact.py b/cyclonedx/model/contact.py index d9367e95..e3f65065 100644 --- a/cyclonedx/model/contact.py +++ b/cyclonedx/model/contact.py @@ -25,7 +25,6 @@ from .._internal.compare import ComparableTuple as _ComparableTuple from ..exception.model import NoPropertiesProvidedException from ..schema.schema import SchemaVersion1Dot6 -from ..serialization import BomRefHelper from . import XsUri from .bom_ref import BomRef @@ -60,7 +59,7 @@ def __init__( @property @serializable.json_name('bom-ref') - @serializable.type_mapping(BomRefHelper) + @serializable.type_mapping(BomRef) @serializable.xml_attribute() @serializable.xml_name('bom-ref') def bom_ref(self) -> Optional[BomRef]: diff --git a/cyclonedx/model/crypto.py b/cyclonedx/model/crypto.py index f692ab19..d9fd8106 100644 --- a/cyclonedx/model/crypto.py +++ b/cyclonedx/model/crypto.py @@ -35,7 +35,6 @@ from .._internal.compare import ComparableTuple as _ComparableTuple from ..exception.model import InvalidNistQuantumSecurityLevelException, InvalidRelatedCryptoMaterialSizeException from ..schema.schema import SchemaVersion1Dot6 -from ..serialization import BomRefHelper from .bom_ref import BomRef @@ -606,7 +605,7 @@ def not_valid_after(self, not_valid_after: Optional[datetime]) -> None: self._not_valid_after = not_valid_after @property - @serializable.type_mapping(BomRefHelper) + @serializable.type_mapping(BomRef) @serializable.xml_sequence(50) def signature_algorithm_ref(self) -> Optional[BomRef]: """ @@ -622,7 +621,7 @@ def signature_algorithm_ref(self, signature_algorithm_ref: Optional[BomRef]) -> self._signature_algorithm_ref = signature_algorithm_ref @property - @serializable.type_mapping(BomRefHelper) + @serializable.type_mapping(BomRef) @serializable.xml_sequence(60) def subject_public_key_ref(self) -> Optional[BomRef]: """ @@ -775,7 +774,7 @@ def mechanism(self, mechanism: Optional[str]) -> None: self._mechanism = mechanism @property - @serializable.type_mapping(BomRefHelper) + @serializable.type_mapping(BomRef) @serializable.xml_sequence(20) def algorithm_ref(self) -> Optional[BomRef]: """ @@ -888,7 +887,7 @@ def state(self, state: Optional[RelatedCryptoMaterialState]) -> None: self._state = state @property - @serializable.type_mapping(BomRefHelper) + @serializable.type_mapping(BomRef) @serializable.xml_sequence(40) def algorithm_ref(self) -> Optional[BomRef]: """ diff --git a/cyclonedx/model/definition.py b/cyclonedx/model/definition.py index 417fd0c1..7c142247 100644 --- a/cyclonedx/model/definition.py +++ b/cyclonedx/model/definition.py @@ -22,7 +22,6 @@ from .._internal.bom_ref import bom_ref_from_str from .._internal.compare import ComparableTuple as _ComparableTuple -from ..serialization import BomRefHelper from . import ExternalReference from .bom_ref import BomRef @@ -71,11 +70,11 @@ def __hash__(self) -> int: def __repr__(self) -> str: return f'' + f'description={self.description}, owner={self.owner}>' @property @serializable.json_name('bom-ref') - @serializable.type_mapping(BomRefHelper) + @serializable.type_mapping(BomRef) @serializable.xml_attribute() @serializable.xml_name('bom-ref') def bom_ref(self) -> BomRef: diff --git a/cyclonedx/model/dependency.py b/cyclonedx/model/dependency.py index 4cdfe17e..58930f3d 100644 --- a/cyclonedx/model/dependency.py +++ b/cyclonedx/model/dependency.py @@ -24,7 +24,6 @@ from .._internal.compare import ComparableTuple as _ComparableTuple from ..exception.serialization import SerializationOfUnexpectedValueException -from ..serialization import BomRefHelper from .bom_ref import BomRef @@ -61,7 +60,7 @@ def __init__(self, ref: BomRef, dependencies: Optional[Iterable['Dependency']] = self.dependencies = dependencies or [] # type:ignore[assignment] @property - @serializable.type_mapping(BomRefHelper) + @serializable.type_mapping(BomRef) @serializable.xml_attribute() def ref(self) -> BomRef: return self._ref diff --git a/cyclonedx/model/license.py b/cyclonedx/model/license.py index fa4f2d33..7814a956 100644 --- a/cyclonedx/model/license.py +++ b/cyclonedx/model/license.py @@ -21,14 +21,17 @@ """ from enum import Enum -from typing import TYPE_CHECKING, Any, Optional, Union +from json import loads as json_loads +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union from warnings import warn +from xml.etree.ElementTree import Element # nosec B405 import serializable from sortedcontainers import SortedSet from .._internal.compare import ComparableTuple as _ComparableTuple from ..exception.model import MutuallyExclusivePropertiesException +from ..exception.serialization import CycloneDxDeserializationException from ..schema.schema import SchemaVersion1Dot6 from . import AttachedText, XsUri @@ -350,6 +353,7 @@ class LicenseRepository(SortedSet[License]): Denormalizers/deserializers will be thankful. The normalization/serialization process SHOULD take care of these facts and do what is needed. """ + else: class LicenseRepository(SortedSet): """Collection of :class:`License`. @@ -364,3 +368,86 @@ class LicenseRepository(SortedSet): Denormalizers/deserializers will be thankful. The normalization/serialization process SHOULD take care of these facts and do what is needed. """ + + +class _LicenseRepositorySerializationHelper(serializable.helpers.BaseHelper): + """ THIS CLASS IS NON-PUBLIC API """ + + @classmethod + def json_normalize(cls, o: LicenseRepository, *, + view: Optional[Type[serializable.ViewType]], + **__: Any) -> Any: + if len(o) == 0: + return None + expression = next((li for li in o if isinstance(li, LicenseExpression)), None) + if expression: + # mixed license expression and license? this is an invalid constellation according to schema! + # see https://github.com/CycloneDX/specification/pull/205 + # but models need to allow it for backwards compatibility with JSON CDX < 1.5 + return [json_loads(expression.as_json(view_=view))] # type:ignore[attr-defined] + return [ + {'license': json_loads( + li.as_json( # type:ignore[attr-defined] + view_=view) + )} + for li in o + if isinstance(li, DisjunctiveLicense) + ] + + @classmethod + def json_denormalize(cls, o: List[Dict[str, Any]], + **__: Any) -> LicenseRepository: + repo = LicenseRepository() + for li in o: + if 'license' in li: + repo.add(DisjunctiveLicense.from_json( # type:ignore[attr-defined] + li['license'])) + elif 'expression' in li: + repo.add(LicenseExpression.from_json( # type:ignore[attr-defined] + li + )) + else: + raise CycloneDxDeserializationException(f'unexpected: {li!r}') + return repo + + @classmethod + def xml_normalize(cls, o: LicenseRepository, *, + element_name: str, + view: Optional[Type[serializable.ViewType]], + xmlns: Optional[str], + **__: Any) -> Optional[Element]: + if len(o) == 0: + return None + elem = Element(element_name) + expression = next((li for li in o if isinstance(li, LicenseExpression)), None) + if expression: + # mixed license expression and license? this is an invalid constellation according to schema! + # see https://github.com/CycloneDX/specification/pull/205 + # but models need to allow it for backwards compatibility with JSON CDX < 1.5 + elem.append(expression.as_xml( # type:ignore[attr-defined] + view_=view, as_string=False, element_name='expression', xmlns=xmlns)) + else: + elem.extend( + li.as_xml( # type:ignore[attr-defined] + view_=view, as_string=False, element_name='license', xmlns=xmlns) + for li in o + if isinstance(li, DisjunctiveLicense) + ) + return elem + + @classmethod + def xml_denormalize(cls, o: Element, + default_ns: Optional[str], + **__: Any) -> LicenseRepository: + repo = LicenseRepository() + for li in o: + tag = li.tag if default_ns is None else li.tag.replace(f'{{{default_ns}}}', '') + if tag == 'license': + repo.add(DisjunctiveLicense.from_xml( # type:ignore[attr-defined] + li, default_ns)) + elif tag == 'expression': + repo.add(LicenseExpression.from_xml( # type:ignore[attr-defined] + li, default_ns)) + else: + raise CycloneDxDeserializationException(f'unexpected: {li!r}') + return repo diff --git a/cyclonedx/model/service.py b/cyclonedx/model/service.py index 9e6af564..7f648386 100644 --- a/cyclonedx/model/service.py +++ b/cyclonedx/model/service.py @@ -29,8 +29,6 @@ import serializable from sortedcontainers import SortedSet -from cyclonedx.serialization import BomRefHelper, LicenseRepositoryHelper - from .._internal.bom_ref import bom_ref_from_str as _bom_ref_from_str from .._internal.compare import ComparableTuple as _ComparableTuple from ..schema.schema import SchemaVersion1Dot3, SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6 @@ -38,7 +36,7 @@ from .bom_ref import BomRef from .contact import OrganizationalEntity from .dependency import Dependable -from .license import License, LicenseRepository +from .license import License, LicenseRepository, _LicenseRepositorySerializationHelper from .release_note import ReleaseNotes @@ -87,7 +85,7 @@ def __init__( @property @serializable.json_name('bom-ref') - @serializable.type_mapping(BomRefHelper) + @serializable.type_mapping(BomRef) @serializable.xml_attribute() @serializable.xml_name('bom-ref') def bom_ref(self) -> BomRef: @@ -263,7 +261,7 @@ def data(self, data: Iterable[DataClassification]) -> None: self._data = SortedSet(data) @property - @serializable.type_mapping(LicenseRepositoryHelper) + @serializable.type_mapping(_LicenseRepositorySerializationHelper) @serializable.xml_sequence(11) def licenses(self) -> LicenseRepository: """ diff --git a/cyclonedx/model/vulnerability.py b/cyclonedx/model/vulnerability.py index 0b7cdc1d..19905ddf 100644 --- a/cyclonedx/model/vulnerability.py +++ b/cyclonedx/model/vulnerability.py @@ -42,7 +42,6 @@ from .._internal.compare import ComparableTuple as _ComparableTuple from ..exception.model import MutuallyExclusivePropertiesException, NoPropertiesProvidedException from ..schema.schema import SchemaVersion1Dot4, SchemaVersion1Dot5, SchemaVersion1Dot6 -from ..serialization import BomRefHelper from . import Property, XsUri from .bom_ref import BomRef from .contact import OrganizationalContact, OrganizationalEntity @@ -982,7 +981,7 @@ def __init__( @property @serializable.json_name('bom-ref') - @serializable.type_mapping(BomRefHelper) + @serializable.type_mapping(BomRef) @serializable.xml_attribute() @serializable.xml_name('bom-ref') def bom_ref(self) -> BomRef: diff --git a/cyclonedx/serialization/__init__.py b/cyclonedx/serialization/__init__.py index 427d0bf6..34489b28 100644 --- a/cyclonedx/serialization/__init__.py +++ b/cyclonedx/serialization/__init__.py @@ -20,10 +20,8 @@ Set of helper classes for use with ``serializable`` when conducting (de-)serialization. """ -from json import loads as json_loads -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type +from typing import Any, Optional from uuid import UUID -from xml.etree.ElementTree import Element # nosec B405 # See https://github.com/package-url/packageurl-python/issues/65 from packageurl import PackageURL @@ -31,29 +29,25 @@ from ..exception.serialization import CycloneDxDeserializationException, SerializationOfUnexpectedValueException from ..model.bom_ref import BomRef -from ..model.license import DisjunctiveLicense, LicenseExpression, LicenseRepository - -if TYPE_CHECKING: # pragma: no cover - from serializable import ViewType +from ..model.license import _LicenseRepositorySerializationHelper class BomRefHelper(BaseHelper): + """**DEPRECATED** in favour of :class:`BomRef`. + + .. deprecated:: 8.6 + Use :class:`BomRef` instead. + """ + + # TODO: remove, no longer needed @classmethod def serialize(cls, o: Any) -> Optional[str]: - if isinstance(o, BomRef): - return o.value - raise SerializationOfUnexpectedValueException( - f'Attempt to serialize a non-BomRef: {o!r}') + return BomRef.serialize(o) @classmethod def deserialize(cls, o: Any) -> BomRef: - try: - return BomRef(value=str(o)) - except ValueError as err: - raise CycloneDxDeserializationException( - f'BomRef string supplied does not parse: {o!r}' - ) from err + return BomRef.deserialize(o) class PackageUrl(BaseHelper): @@ -94,82 +88,13 @@ def deserialize(cls, o: Any) -> UUID: ) from err -class LicenseRepositoryHelper(BaseHelper): - @classmethod - def json_normalize(cls, o: LicenseRepository, *, - view: Optional[Type['ViewType']], - **__: Any) -> Any: - if len(o) == 0: - return None - expression = next((li for li in o if isinstance(li, LicenseExpression)), None) - if expression: - # mixed license expression and license? this is an invalid constellation according to schema! - # see https://github.com/CycloneDX/specification/pull/205 - # but models need to allow it for backwards compatibility with JSON CDX < 1.5 - return [json_loads(expression.as_json(view_=view))] # type:ignore[attr-defined] - return [ - {'license': json_loads( - li.as_json( # type:ignore[attr-defined] - view_=view) - )} - for li in o - if isinstance(li, DisjunctiveLicense) - ] +class LicenseRepositoryHelper(_LicenseRepositorySerializationHelper): + """**DEPRECATED** - @classmethod - def json_denormalize(cls, o: List[Dict[str, Any]], - **__: Any) -> LicenseRepository: - repo = LicenseRepository() - for li in o: - if 'license' in li: - repo.add(DisjunctiveLicense.from_json( # type:ignore[attr-defined] - li['license'])) - elif 'expression' in li: - repo.add(LicenseExpression.from_json( # type:ignore[attr-defined] - li - )) - else: - raise CycloneDxDeserializationException(f'unexpected: {li!r}') - return repo + .. deprecated:: 8.6 + No public API planned for replacing this, + """ - @classmethod - def xml_normalize(cls, o: LicenseRepository, *, - element_name: str, - view: Optional[Type['ViewType']], - xmlns: Optional[str], - **__: Any) -> Optional[Element]: - if len(o) == 0: - return None - elem = Element(element_name) - expression = next((li for li in o if isinstance(li, LicenseExpression)), None) - if expression: - # mixed license expression and license? this is an invalid constellation according to schema! - # see https://github.com/CycloneDX/specification/pull/205 - # but models need to allow it for backwards compatibility with JSON CDX < 1.5 - elem.append(expression.as_xml( # type:ignore[attr-defined] - view_=view, as_string=False, element_name='expression', xmlns=xmlns)) - else: - elem.extend( - li.as_xml( # type:ignore[attr-defined] - view_=view, as_string=False, element_name='license', xmlns=xmlns) - for li in o - if isinstance(li, DisjunctiveLicense) - ) - return elem + # TODO: remove, no longer needed - @classmethod - def xml_denormalize(cls, o: Element, - default_ns: Optional[str], - **__: Any) -> LicenseRepository: - repo = LicenseRepository() - for li in o: - tag = li.tag if default_ns is None else li.tag.replace(f'{{{default_ns}}}', '') - if tag == 'license': - repo.add(DisjunctiveLicense.from_xml( # type:ignore[attr-defined] - li, default_ns)) - elif tag == 'expression': - repo.add(LicenseExpression.from_xml( # type:ignore[attr-defined] - li, default_ns)) - else: - raise CycloneDxDeserializationException(f'unexpected: {li!r}') - return repo + pass