diff --git a/cyclonedx/validation/json.py b/cyclonedx/validation/json.py index dbd3679b..678a9d94 100644 --- a/cyclonedx/validation/json.py +++ b/cyclonedx/validation/json.py @@ -16,7 +16,7 @@ # Copyright (c) OWASP Foundation. All Rights Reserved. -__all__ = ['JsonValidator', 'JsonStrictValidator'] +__all__ = ['JsonValidator', 'JsonStrictValidator', 'JsonValidationError'] from abc import ABC from collections.abc import Iterable @@ -40,6 +40,7 @@ from referencing.jsonschema import DRAFT7 if TYPE_CHECKING: # pragma: no cover + from jsonschema.exceptions import ValidationError as JsonSchemaValidationError # type:ignore[import-untyped] from jsonschema.protocols import Validator as JsonSchemaValidator # type:ignore[import-untyped] except ImportError as err: _missing_deps_error = MissingOptionalDependencyException( @@ -48,6 +49,14 @@ ), err +class JsonValidationError(ValidationError): + @classmethod + def _make_from_jsve(cls, e: 'JsonSchemaValidationError') -> 'JsonValidationError': + """⚠️ This is an internal API. It is not part of the public interface and may change without notice.""" + # in preparation for https://github.com/CycloneDX/cyclonedx-python-lib/pull/836 + return cls(e) + + class _BaseJsonValidator(BaseSchemabasedValidator, ABC): @property def output_format(self) -> Literal[OutputFormat.JSON]: @@ -60,16 +69,16 @@ def __init__(self, schema_version: 'SchemaVersion') -> None: # region typing-relevant copy from parent class - needed for mypy and doc tools @overload - def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[ValidationError]: + def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[JsonValidationError]: ... # pragma: no cover @overload - def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[ValidationError]]: + def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[JsonValidationError]]: ... # pragma: no cover def validate_str( self, data: str, *, all_errors: bool = False - ) -> Union[None, ValidationError, Iterable[ValidationError]]: + ) -> Union[None, JsonValidationError, Iterable[JsonValidationError]]: ... # pragma: no cover # endregion @@ -79,22 +88,22 @@ def validate_str( def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first self, data: str, *, all_errors: bool = False - ) -> Union[None, ValidationError, Iterable[ValidationError]]: + ) -> Union[None, JsonValidationError, Iterable[JsonValidationError]]: raise self.__MDERROR[0] from self.__MDERROR[1] else: def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first self, data: str, *, all_errors: bool = False - ) -> Union[None, ValidationError, Iterable[ValidationError]]: + ) -> Union[None, JsonValidationError, Iterable[JsonValidationError]]: validator = self._validator # may throw on error that MUST NOT be caught structure = json_loads(data) errors = validator.iter_errors(structure) first_error = next(errors, None) if first_error is None: return None - first_error = ValidationError(first_error) - return chain((first_error,), map(ValidationError, errors)) \ + first_error = JsonValidationError._make_from_jsve(first_error) + return chain((first_error,), map(JsonValidationError._make_from_jsve, errors)) \ if all_errors \ else first_error diff --git a/cyclonedx/validation/xml.py b/cyclonedx/validation/xml.py index 7dca07c3..14f52808 100644 --- a/cyclonedx/validation/xml.py +++ b/cyclonedx/validation/xml.py @@ -16,7 +16,7 @@ # Copyright (c) OWASP Foundation. All Rights Reserved. -__all__ = ['XmlValidator'] +__all__ = ['XmlValidator', 'XmlValidationError'] from abc import ABC from collections.abc import Iterable @@ -37,6 +37,9 @@ XMLSchema, fromstring as xml_fromstring, ) + + if TYPE_CHECKING: # pragma: no cover + from lxml.etree import _LogEntry as _XmlLogEntry except ImportError as err: _missing_deps_error = MissingOptionalDependencyException( 'This functionality requires optional dependencies.\n' @@ -44,6 +47,14 @@ ), err +class XmlValidationError(ValidationError): + @classmethod + def _make_from_xle(cls, e: '_XmlLogEntry') -> 'XmlValidationError': + """⚠️ This is an internal API. It is not part of the public interface and may change without notice.""" + # in preparation for https://github.com/CycloneDX/cyclonedx-python-lib/pull/836 + return cls(e) + + class _BaseXmlValidator(BaseSchemabasedValidator, ABC): @property @@ -57,16 +68,16 @@ def __init__(self, schema_version: 'SchemaVersion') -> None: # region typing-relevant copy from parent class - needed for mypy and doc tools @overload - def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[ValidationError]: + def validate_str(self, data: str, *, all_errors: Literal[False] = ...) -> Optional[XmlValidationError]: ... # pragma: no cover @overload - def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[ValidationError]]: + def validate_str(self, data: str, *, all_errors: Literal[True]) -> Optional[Iterable[XmlValidationError]]: ... # pragma: no cover def validate_str( self, data: str, *, all_errors: bool = False - ) -> Union[None, ValidationError, Iterable[ValidationError]]: + ) -> Union[None, XmlValidationError, Iterable[XmlValidationError]]: ... # pragma: no cover # endregion typing-relevant @@ -76,13 +87,13 @@ def validate_str( def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first self, data: str, *, all_errors: bool = False - ) -> Union[None, ValidationError, Iterable[ValidationError]]: + ) -> Union[None, XmlValidationError, Iterable[XmlValidationError]]: raise self.__MDERROR[0] from self.__MDERROR[1] else: def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers go first self, data: str, *, all_errors: bool = False - ) -> Union[None, ValidationError, Iterable[ValidationError]]: + ) -> Union[None, XmlValidationError, Iterable[XmlValidationError]]: validator = self._validator # may throw on error that MUST NOT be caught valid = validator.validate( xml_fromstring( # nosec B320 -- we use a custom prepared safe parser @@ -91,9 +102,9 @@ def validate_str( # type:ignore[no-redef] # noqa:F811 # typing-relevant headers if valid: return None errors = validator.error_log - return map(ValidationError, errors) \ + return map(XmlValidationError._make_from_xle, errors) \ if all_errors \ - else ValidationError(errors.last_error) + else XmlValidationError._make_from_xle(errors.last_error) __validator: Optional['XMLSchema'] = None diff --git a/examples/complex_deserialize.py b/examples/complex_deserialize.py index d139aa01..097a3cc9 100644 --- a/examples/complex_deserialize.py +++ b/examples/complex_deserialize.py @@ -147,9 +147,9 @@ }""" my_json_validator = JsonStrictValidator(SchemaVersion.V1_6) try: - validation_errors = my_json_validator.validate_str(json_data) - if validation_errors: - print('JSON invalid', 'ValidationError:', repr(validation_errors), sep='\n', file=sys.stderr) + json_validation_errors = my_json_validator.validate_str(json_data) + if json_validation_errors: + print('JSON invalid', 'ValidationError:', repr(json_validation_errors), sep='\n', file=sys.stderr) sys.exit(2) print('JSON valid') except MissingOptionalDependencyException as error: @@ -248,9 +248,9 @@ """ my_xml_validator: 'XmlValidator' = make_schemabased_validator(OutputFormat.XML, SchemaVersion.V1_6) try: - validation_errors = my_xml_validator.validate_str(xml_data) - if validation_errors: - print('XML invalid', 'ValidationError:', repr(validation_errors), sep='\n', file=sys.stderr) + xml_validation_errors = my_xml_validator.validate_str(xml_data) + if xml_validation_errors: + print('XML invalid', 'ValidationError:', repr(xml_validation_errors), sep='\n', file=sys.stderr) sys.exit(2) print('XML valid') except MissingOptionalDependencyException as error: diff --git a/examples/complex_serialize.py b/examples/complex_serialize.py index e69d186d..771dbc47 100644 --- a/examples/complex_serialize.py +++ b/examples/complex_serialize.py @@ -91,9 +91,9 @@ print(serialized_json) my_json_validator = JsonStrictValidator(SchemaVersion.V1_6) try: - validation_errors = my_json_validator.validate_str(serialized_json) - if validation_errors: - print('JSON invalid', 'ValidationError:', repr(validation_errors), sep='\n', file=sys.stderr) + json_validation_errors = my_json_validator.validate_str(serialized_json) + if json_validation_errors: + print('JSON invalid', 'ValidationError:', repr(json_validation_errors), sep='\n', file=sys.stderr) sys.exit(2) print('JSON valid') except MissingOptionalDependencyException as error: @@ -112,9 +112,9 @@ my_xml_validator: 'XmlValidator' = make_schemabased_validator( my_xml_outputter.output_format, my_xml_outputter.schema_version) try: - validation_errors = my_xml_validator.validate_str(serialized_xml) - if validation_errors: - print('XML invalid', 'ValidationError:', repr(validation_errors), sep='\n', file=sys.stderr) + xml_validation_errors = my_xml_validator.validate_str(serialized_xml) + if xml_validation_errors: + print('XML invalid', 'ValidationError:', repr(xml_validation_errors), sep='\n', file=sys.stderr) sys.exit(2) print('XML valid') except MissingOptionalDependencyException as error: