From 2de641a87d84c841e65758dd1b40f6f2beda8bb2 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 12 Sep 2022 23:02:46 +0200 Subject: [PATCH] feat: license factories Signed-off-by: Jan Kowalleck --- .gitignore | 4 +- cyclonedx/exception/factory.py | 45 ++++++++++++++++++ cyclonedx/factory/__init__.py | 16 +++++++ cyclonedx/factory/license.py | 83 ++++++++++++++++++++++++++++++++++ cyclonedx/spdx.py | 47 +++++++++++++++++++ 5 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 cyclonedx/exception/factory.py create mode 100644 cyclonedx/factory/__init__.py create mode 100644 cyclonedx/factory/license.py create mode 100644 cyclonedx/spdx.py diff --git a/.gitignore b/.gitignore index a651d288b..28c197135 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ html/ /.mypy_cache # Exlude built docs -docs/_build +docs/_build/ +docs/autoapi/ + diff --git a/cyclonedx/exception/factory.py b/cyclonedx/exception/factory.py new file mode 100644 index 000000000..93c6b1da1 --- /dev/null +++ b/cyclonedx/exception/factory.py @@ -0,0 +1,45 @@ +# encoding: utf-8 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +""" +Exceptions relating to specific conditions that occur when factoring a model. +""" + +from . import CycloneDxException + + +class CycloneDxFactoryException(CycloneDxException): + """ + Base exception that covers all exceptions that may be thrown during model factoring.. + """ + pass + + +class LicenseChoiceFactoryException(CycloneDxFactoryException): + pass + + +class InvalidSpdxLicenseException(LicenseChoiceFactoryException): + pass + + +class LicenseFactoryException(CycloneDxFactoryException): + pass + + +class InvalidLicenseExpressionException(LicenseFactoryException): + pass diff --git a/cyclonedx/factory/__init__.py b/cyclonedx/factory/__init__.py new file mode 100644 index 000000000..d526f9b6d --- /dev/null +++ b/cyclonedx/factory/__init__.py @@ -0,0 +1,16 @@ +# encoding: utf-8 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. diff --git a/cyclonedx/factory/license.py b/cyclonedx/factory/license.py new file mode 100644 index 000000000..0dedc4e77 --- /dev/null +++ b/cyclonedx/factory/license.py @@ -0,0 +1,83 @@ +# encoding: utf-8 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +from typing import Optional + +from ..exception.factory import InvalidLicenseExpressionException, InvalidSpdxLicenseException +from ..model import AttachedText, License, LicenseChoice, XsUri +from ..spdx import fixup as spdx_fixup + + +class LicenseFactory: + """Factory for :class:`cyclonedx.model.License`.""" + + def make_from_string(self, name_or_spdx: str, *, + license_text: Optional[AttachedText] = None, + license_url: Optional[XsUri] = None) -> License: + """Make a :class:`cyclonedx.model.License` from a string.""" + try: + return self.make_with_id(name_or_spdx, license_text=license_text, license_url=license_url) + except InvalidSpdxLicenseException: + return self.make_with_name(name_or_spdx, license_text=license_text, license_url=license_url) + + def make_with_id(self, spdx_id: str, *, + license_text: Optional[AttachedText] = None, + license_url: Optional[XsUri] = None) -> License: + """Make a :class:`cyclonedx.model.License` from a SPDX-ID. + + :raises InvalidSpdxLicenseException: if `spdx_id` was not known/supported SPDX license ID + """ + spdx_license_id = spdx_fixup(spdx_id) + if spdx_license_id is None: + raise InvalidSpdxLicenseException(spdx_id) + return License(spdx_license_id=spdx_license_id, license_text=license_text, license_url=license_url) + + def make_with_name(self, name: str, *, + license_text: Optional[AttachedText] = None, + license_url: Optional[XsUri] = None) -> License: + """Make a :class:`cyclonedx.model.License` with a name.""" + return License(license_name=name, license_text=license_text, license_url=license_url) + + +class LicenseChoiceFactory: + """Factory for :class:`cyclonedx.model.LicenseChoice`.""" + + def __init__(self, *, license_factory: LicenseFactory) -> None: + self.license_factory = license_factory + + def make_from_string(self, expression_or_name_or_spdx: str) -> LicenseChoice: + """Make a :class:`cyclonedx.model.LicenseChoice` from a string.""" + try: + return self.make_with_expression(expression_or_name_or_spdx) + except InvalidLicenseExpressionException: + return self.make_with_license(expression_or_name_or_spdx) + + def make_with_expression(self, expression: str) -> LicenseChoice: + """Make a :class:`cyclonedx.model.LicenseChoice` with an expression. + + :raises InvalidLicenseExpressionException: if `expression` is not known/supported license expression + """ + if expression.startswith('(') and expression.endswith(')'): + return LicenseChoice(license_expression=expression) + raise InvalidLicenseExpressionException(expression) + + def make_with_license(self, name_or_spdx: str, *, + license_text: Optional[AttachedText] = None, + license_url: Optional[XsUri] = None) -> LicenseChoice: + """Make a :class:`cyclonedx.model.LicenseChoice` with a license (name or SPDX-ID).""" + return LicenseChoice(license_=self.license_factory.make_from_string( + name_or_spdx, license_text=license_text, license_url=license_url)) diff --git a/cyclonedx/spdx.py b/cyclonedx/spdx.py new file mode 100644 index 000000000..58fe9f8ef --- /dev/null +++ b/cyclonedx/spdx.py @@ -0,0 +1,47 @@ +# encoding: utf-8 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +__all__ = ['is_supported', 'fixup'] + +from json import load as json_load +from os.path import dirname, join as path_join +from typing import Dict, Optional, Set + +# region init +# python's internal module loader will assure that this init-part runs only once. + +# !!! this requires to ship the actual schema data with the package. +with open(path_join(dirname(__file__), 'schema', 'spdx.schema.json')) as f: + __ids: Set[str] = set(json_load(f).get('enum', [])) +if len(__ids) == 0: + raise ValueError('failed to load SPDX IDs') +__ids_lower_map: Dict[str, str] = dict((v.lower(), v) for v in __ids) + + +# endregion + + +def is_supported(value: str) -> bool: + """Validate a SPDX-ID according to current spec.""" + return value in __ids + + +def fixup(value: str) -> Optional[str]: + """Fixup a SPDX-ID. + + :returns: repaired value string, or `None` if fixup was unable to help. + """ + return __ids_lower_map.get(value.lower())