From 0798f7595f2bd1badbeddb283c83dcd13c26f607 Mon Sep 17 00:00:00 2001 From: Behzad Motaghi Date: Thu, 12 Nov 2026 21:30:43 +0100 Subject: [PATCH] Reject duplicate extracted-license identifiers Fixes #809 by checking license_id uniqueness in extracted-license list validation, with duplicate and distinct IDs tested through both list and full-document validation. Signed-off-by: Behzad Motaghi --- .../extracted_licensing_info_validator.py | 12 ++++++ .../validation/test_document_validator.py | 37 ++++++++++++++++++- ...test_extracted_licensing_info_validator.py | 32 +++++++++++++++- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/spdx_tools/spdx/validation/extracted_licensing_info_validator.py b/src/spdx_tools/spdx/validation/extracted_licensing_info_validator.py index 6cca07576..349b053cf 100644 --- a/src/spdx_tools/spdx/validation/extracted_licensing_info_validator.py +++ b/src/spdx_tools/spdx/validation/extracted_licensing_info_validator.py @@ -15,8 +15,20 @@ def validate_extracted_licensing_infos( extracted_licensing_infos: Optional[List[ExtractedLicensingInfo]], ) -> List[ValidationMessage]: validation_messages = [] + seen_license_ids = set() for extracted_licensing_info in extracted_licensing_infos: validation_messages.extend(validate_extracted_licensing_info(extracted_licensing_info)) + license_id = extracted_licensing_info.license_id + if license_id and license_id in seen_license_ids: + validation_messages.append( + ValidationMessage( + f"license_id must be unique within the document, but is repeated: {license_id}", + ValidationContext( + element_type=SpdxElementType.EXTRACTED_LICENSING_INFO, full_element=extracted_licensing_info + ), + ) + ) + seen_license_ids.add(license_id) return validation_messages diff --git a/tests/spdx/validation/test_document_validator.py b/tests/spdx/validation/test_document_validator.py index 1b1897f52..b04882f59 100644 --- a/tests/spdx/validation/test_document_validator.py +++ b/tests/spdx/validation/test_document_validator.py @@ -11,7 +11,14 @@ from spdx_tools.spdx.parser.parse_anything import parse_file from spdx_tools.spdx.validation.document_validator import validate_full_spdx_document from spdx_tools.spdx.validation.validation_message import SpdxElementType, ValidationContext, ValidationMessage -from tests.spdx.fixtures import creation_info_fixture, document_fixture, file_fixture, package_fixture, snippet_fixture +from tests.spdx.fixtures import ( + creation_info_fixture, + document_fixture, + extracted_licensing_info_fixture, + file_fixture, + package_fixture, + snippet_fixture, +) def test_valid_document(): @@ -21,6 +28,34 @@ def test_valid_document(): assert validation_messages == [] +@pytest.mark.parametrize( + "license_id, expected_message", + [ + ("LicenseRef-1", "license_id must be unique within the document, but is repeated: LicenseRef-1"), + ("LicenseRef-2", None), + ], +) +def test_document_extracted_licensing_info_unique_ids(license_id, expected_message): + extracted_licensing_info = extracted_licensing_info_fixture(license_id=license_id, extracted_text="different text") + document = document_fixture( + extracted_licensing_info=[extracted_licensing_info_fixture(), extracted_licensing_info] + ) + validation_messages = validate_full_spdx_document(document) + + expected = [] + if expected_message: + expected.append( + ValidationMessage( + expected_message, + ValidationContext( + element_type=SpdxElementType.EXTRACTED_LICENSING_INFO, full_element=extracted_licensing_info + ), + ) + ) + + assert validation_messages == expected + + def test_spdx_lite_validation(): document = parse_file(os.path.join(os.path.dirname(__file__), "../data/SPDXLite.spdx")) diff --git a/tests/spdx/validation/test_extracted_licensing_info_validator.py b/tests/spdx/validation/test_extracted_licensing_info_validator.py index 911bea71c..468962b6d 100644 --- a/tests/spdx/validation/test_extracted_licensing_info_validator.py +++ b/tests/spdx/validation/test_extracted_licensing_info_validator.py @@ -6,7 +6,10 @@ import pytest -from spdx_tools.spdx.validation.extracted_licensing_info_validator import validate_extracted_licensing_info +from spdx_tools.spdx.validation.extracted_licensing_info_validator import ( + validate_extracted_licensing_info, + validate_extracted_licensing_infos, +) from spdx_tools.spdx.validation.validation_message import SpdxElementType, ValidationContext, ValidationMessage from tests.spdx.fixtures import extracted_licensing_info_fixture @@ -18,6 +21,33 @@ def test_valid_extracted_licensing_info(): assert validation_messages == [] +@pytest.mark.parametrize( + "license_id, expected_message", + [ + ("LicenseRef-1", "license_id must be unique within the document, but is repeated: LicenseRef-1"), + ("LicenseRef-2", None), + ], +) +def test_extracted_licensing_infos_unique_ids(license_id, expected_message): + extracted_licensing_info = extracted_licensing_info_fixture(license_id=license_id, extracted_text="different text") + validation_messages = validate_extracted_licensing_infos( + [extracted_licensing_info_fixture(), extracted_licensing_info] + ) + + expected = [] + if expected_message: + expected.append( + ValidationMessage( + expected_message, + ValidationContext( + element_type=SpdxElementType.EXTRACTED_LICENSING_INFO, full_element=extracted_licensing_info + ), + ) + ) + + assert validation_messages == expected + + # TODO: tests for licenses not on the SPDX License list (i.e. they must provide id, name and cross-references) @pytest.mark.parametrize( "extracted_licensing_info, expected_message",