diff --git a/cyclonedx/spdx.py b/cyclonedx/spdx.py index 52621c5d..8e59f376 100644 --- a/cyclonedx/spdx.py +++ b/cyclonedx/spdx.py @@ -14,11 +14,19 @@ # # SPDX-License-Identifier: Apache-2.0 -__all__ = ['is_supported_id', 'fixup_id', 'is_compound_expression'] +__all__ = [ + 'is_supported_id', 'fixup_id', + 'is_compound_expression' +] from json import load as json_load from os.path import dirname, join as path_join -from typing import Dict, Optional, Set +from typing import TYPE_CHECKING, Dict, Optional, Set + +from license_expression import get_spdx_licensing # type: ignore + +if TYPE_CHECKING: + from license_expression import Licensing # region init # python's internal module loader will assure that this init-part runs only once. @@ -30,9 +38,11 @@ __IDS_LOWER_MAP: Dict[str, str] = dict((id_.lower(), id_) for id_ in __IDS) +__SPDX_EXPRESSION_LICENSING: 'Licensing' = get_spdx_licensing() # endregion + def is_supported_id(value: str) -> bool: """Validate a SPDX-ID according to current spec.""" return value in __IDS @@ -50,11 +60,12 @@ def is_compound_expression(value: str) -> bool: """Validate compound expression. .. note:: - Uses a best-effort detection of SPDX compound expression according to `SPDX license expression spec`_. + Utilizes `license-expression library`_ to + validate SPDX compound expression according to `SPDX license expression spec`_. .. _SPDX license expression spec: https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/ + .. _license-expression library: https://github.com/nexB/license-expression """ - # shortest known valid expression: (A or B) - 8 characters long - return len(value) >= 8 \ - and value.startswith('(') \ - and value.endswith(')') + return 0 == len( + __SPDX_EXPRESSION_LICENSING.validate(value).errors + ) diff --git a/poetry.lock b/poetry.lock index ad7e3e50..47ef2159 100644 --- a/poetry.lock +++ b/poetry.lock @@ -19,6 +19,18 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib- tests = ["attrs[tests-no-zope]", "zope.interface"] tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] +[[package]] +name = "boolean-py" +version = "4.0" +description = "Define boolean algebras, create and parse boolean expressions and create custom boolean DSL." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "boolean.py-4.0-py3-none-any.whl", hash = "sha256:2876f2051d7d6394a531d82dc6eb407faa0b01a0a0b3083817ccd7323b8d96bd"}, + {file = "boolean.py-4.0.tar.gz", hash = "sha256:17b9a181630e43dde1851d42bef546d616d5d9b4480357514597e78b203d06e4"}, +] + [[package]] name = "colorama" version = "0.4.6" @@ -310,6 +322,25 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] +[[package]] +name = "license-expression" +version = "30.1.1" +description = "license-expression is a comprehensive utility library to parse, compare, simplify and normalize license expressions (such as SPDX license expressions) using boolean logic." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "license-expression-30.1.1.tar.gz", hash = "sha256:42375df653ad85e6f5b4b0385138b2dbea1f5d66360783d8625c3e4f97f11f0c"}, + {file = "license_expression-30.1.1-py3-none-any.whl", hash = "sha256:8d7e5e2de0d04fc104a4f952c440e8f08a5ba63480a0dad015b294770b7e58ec"}, +] + +[package.dependencies] +"boolean.py" = ">=4.0" + +[package.extras] +docs = ["Sphinx (==5.1.0)", "doc8 (>=0.8.1)", "sphinx-rtd-theme (>=0.5.0)", "sphinxcontrib-apidoc (>=0.3.0)"] +testing = ["black", "isort", "pytest (>=6,!=7.0.0)", "pytest-xdist (>=2)", "twine"] + [[package]] name = "lxml" version = "4.9.3" @@ -860,4 +891,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "459de3873dc790f7ab293ba50f0bd991a14c3a65147a5fc2b111809f128fc3e9" +content-hash = "fdc1e22d03f0e1e180558a0237ab46e94e512ce018b64419099f0d0915a39cf9" diff --git a/pyproject.toml b/pyproject.toml index fa882f32..3b9124b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ python = "^3.7" packageurl-python = ">= 0.11" py-serializable = "^0.11.1" sortedcontainers = "^2.4.0" +license-expression = "^30" [tool.poetry.dev-dependencies] ddt = "^1.6.0" diff --git a/tests/test_spdx.py b/tests/test_spdx.py index ff4c951b..d4399e31 100644 --- a/tests/test_spdx.py +++ b/tests/test_spdx.py @@ -33,8 +33,8 @@ VALID_COMPOUND_EXPRESSIONS = { # for valid test data see the spec: https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/ - '(MIT WITH Apache-2.0)', - '(BSD-2-Clause OR Apache-2.0)', + '(MIT AND Apache-2.0)', + 'BSD-2-Clause OR Apache-2.0', } @@ -89,8 +89,9 @@ def test_positive(self, valid_expression: str) -> None: self.assertTrue(actual) @data( + 'MIT AND Apache-2.0 OR something-unknown' 'something invalid', - '(c) John Doe' + '(c) John Doe', ) def test_negative(self, invalid_expression: str) -> None: actual = spdx.is_compound_expression(invalid_expression)