From 59d6a2f5acda4391d248b72b95c2e387118648eb Mon Sep 17 00:00:00 2001 From: Tieg Zaharia Date: Tue, 29 Sep 2026 13:18:56 -0700 Subject: [PATCH 1/2] Fix Spdx2to3Converter dropping packages/files/snippets with no relationships Spdx2to3Converter.convertAndStore(SpdxDocument) only discovered elements by walking documentDescribes and following Relationships. Any package, file, or snippet that existed in the source document but had no relationship to anything else and wasn't part of documentDescribes was never visited, so it was silently omitted from the converted SPDX 3 output. Add a pass after the existing conversion that scans the source model store for any SpdxPackage/SpdxFile/SpdxSnippet not already converted and converts them, adding them as root elements of the resulting SpdxDocument. --- .../library/conversion/Spdx2to3Converter.java | 30 +++++++++++ .../conversion/Spdx2to3ConverterTest.java | 52 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/src/main/java/org/spdx/library/conversion/Spdx2to3Converter.java b/src/main/java/org/spdx/library/conversion/Spdx2to3Converter.java index f83f32f8..43facd7d 100644 --- a/src/main/java/org/spdx/library/conversion/Spdx2to3Converter.java +++ b/src/main/java/org/spdx/library/conversion/Spdx2to3Converter.java @@ -21,6 +21,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nullable; @@ -29,6 +30,7 @@ import org.spdx.core.IModelCopyManager; import org.spdx.core.InvalidSPDXAnalysisException; import org.spdx.library.ListedLicenses; +import org.spdx.library.SpdxModelFactory; import org.spdx.library.model.v2.SpdxConstantsCompatV2; import org.spdx.library.model.v2.SpdxCreatorInformation; import org.spdx.library.model.v2.pointer.ByteOffsetPointer; @@ -700,9 +702,37 @@ public SpdxDocument convertAndStore(org.spdx.library.model.v2.SpdxDocument fromD for (org.spdx.library.model.v2.license.ExtractedLicenseInfo extractedLicense:fromDoc.getExtractedLicenseInfos()) { convertAndStore(extractedLicense); } + convertOrphanElements(fromDoc, toDoc); return toDoc; } + /** + * Converts any SPDX spec version 2 packages, files, and snippets belonging to fromDoc which are not + * reachable through documentDescribes or a relationship (i.e. have no edges connecting them to anything + * else in the document). Without this, such elements would be silently dropped from the conversion since + * they would never be visited by the relationship graph traversal performed elsewhere in this class. + * Any such elements found are converted, stored, and added to the toDoc's root elements. + * @param fromDoc SPDX spec version 2 document being converted from + * @param toDoc SPDX spec version 3 document being converted to + * @throws InvalidSPDXAnalysisException on any errors converting the orphan elements + */ + @SuppressWarnings("unchecked") + private void convertOrphanElements(org.spdx.library.model.v2.SpdxDocument fromDoc, SpdxDocument toDoc) throws InvalidSPDXAnalysisException { + String documentUri = fromDoc.getDocumentUri(); + IModelStore fromModelStore = fromDoc.getModelStore(); + for (String typeFilter : new String[] {SpdxConstantsCompatV2.CLASS_SPDX_PACKAGE, + SpdxConstantsCompatV2.CLASS_SPDX_FILE, SpdxConstantsCompatV2.CLASS_SPDX_SNIPPET}) { + List fromElements = ((Stream) + SpdxModelFactory.getSpdxObjects(fromModelStore, copyManager, typeFilter, documentUri, null)) + .collect(Collectors.toList()); + for (org.spdx.library.model.v2.SpdxElement fromElement : fromElements) { + if (!alreadyCopied(fromElement.getObjectUri())) { + toDoc.getRootElements().add(convertAndStore(fromElement)); + } + } + } + } + /** * Converts the externalDocRef to a NamespaceMap and store the NamespaceMap. * The document information is also retained in the externalDocRefMap such that any subsequent diff --git a/src/test/java/org/spdx/library/conversion/Spdx2to3ConverterTest.java b/src/test/java/org/spdx/library/conversion/Spdx2to3ConverterTest.java index 30f8f507..3fb32ca6 100644 --- a/src/test/java/org/spdx/library/conversion/Spdx2to3ConverterTest.java +++ b/src/test/java/org/spdx/library/conversion/Spdx2to3ConverterTest.java @@ -23,10 +23,12 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import javax.annotation.Nullable; @@ -671,6 +673,56 @@ public void testConvertAndStoreSpdxDocument() throws InvalidSPDXAnalysisExceptio assertTrue(verify.isEmpty()); } + /** + * Test method for {@link org.spdx.library.conversion.Spdx2to3Converter#convertAndStore(org.spdx.library.model.v2.SpdxDocument)}. + * Verifies that a package with no relationships and not included in documentDescribes (i.e. no edges + * connecting it to anything else in the document) is still converted rather than silently dropped. + * @throws InvalidSPDXAnalysisException + */ + @Test + public void testConvertAndStoreSpdxDocumentOrphanPackage() throws InvalidSPDXAnalysisException { + String describedPkgName = "described package"; + String orphanPkgName = "orphan package"; + + org.spdx.library.model.v2.SpdxDocument doc = new org.spdx.library.model.v2.SpdxDocument(fromModelStore, DOCUMENT_URI, copyManager, true); + doc.setCreationInfo(doc.createCreationInfo(Arrays.asList(new String[] {SpdxConstantsCompatV2.CREATOR_PREFIX_TOOL + "test"}), + "2010-01-29T18:30:22Z")); + org.spdx.library.model.v2.license.AnyLicenseInfo dataLicense = + LicenseInfoFactory.parseSPDXLicenseStringCompatV2("CC0-1.0", fromModelStore, DOCUMENT_URI, copyManager); + doc.setDataLicense(dataLicense); + + org.spdx.library.model.v2.license.AnyLicenseInfo noAssertion = + new org.spdx.library.model.v2.license.SpdxNoAssertionLicense(); + org.spdx.library.model.v2.SpdxPackage describedPkg = doc.createPackage(fromModelStore.getNextId(IdType.SpdxId), + describedPkgName, noAssertion, "copyright", noAssertion) + .setFilesAnalyzed(false) + .setDownloadLocation("NOASSERTION") + .build(); + doc.setDocumentDescribes(Arrays.asList(new org.spdx.library.model.v2.SpdxItem[] {describedPkg})); + + // orphan package - no relationships, not part of documentDescribes + org.spdx.library.model.v2.SpdxPackage orphanPkg = doc.createPackage(fromModelStore.getNextId(IdType.SpdxId), + orphanPkgName, noAssertion, "copyright", noAssertion) + .setFilesAnalyzed(false) + .setDownloadLocation("NOASSERTION") + .build(); + + Spdx2to3Converter converter = new Spdx2to3Converter(toModelStore, copyManager, defaultCreationInfo, + SpdxModelFactory.getLatestSpecVersion(), DEFAULT_PREFIX, true); + converter.convertAndStore(doc); + + List resultPackages = new ArrayList<>(); + SpdxModelFactory.getSpdxObjects(toModelStore, copyManager, SpdxConstantsV3.SOFTWARE_SPDX_PACKAGE, DEFAULT_PREFIX, DEFAULT_PREFIX) + .forEach(pkg -> resultPackages.add((SpdxPackage)pkg)); + assertEquals(2, resultPackages.size()); + Set resultPackageNames = new HashSet<>(); + for (SpdxPackage pkg:resultPackages) { + resultPackageNames.add(pkg.getName().get()); + } + assertTrue(resultPackageNames.contains(describedPkgName)); + assertTrue(resultPackageNames.contains(orphanPkgName)); + } + @Test public void testConvertAndStoreExternalDocRef() throws InvalidSPDXAnalysisException { String externalDocumentId = SpdxConstantsCompatV2.EXTERNAL_DOC_REF_PRENUM + "external"; From 11cd69db287bbfba97ec9fd2b3ab21e16cbf29c2 Mon Sep 17 00:00:00 2001 From: Tieg Zaharia Date: Thu, 1 Oct 2026 13:18:18 -0700 Subject: [PATCH 2/2] Also convert orphan extracted/listed licenses and exceptions as root elements Extracted licensing info, listed licenses, and listed license exceptions that exist in a source document's model store but are not reachable from documentDescribes, a relationship, or any referenced license expression were previously dropped from the SPDX 3 output, since they were never added to the document's root elements. - Add extracted licenses to the root elements list directly, since they are already unconditionally converted from the document's extractedLicenseInfos. - Scan the source model store for listed licenses and listed license exceptions not already converted, converting and rooting any orphans found (searched without a document URI prefix since these are stored under the SPDX License List namespace). --- .../library/conversion/Spdx2to3Converter.java | 29 +++++- .../conversion/Spdx2to3ConverterTest.java | 97 ++++++++++++++++++- 2 files changed, 118 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/spdx/library/conversion/Spdx2to3Converter.java b/src/main/java/org/spdx/library/conversion/Spdx2to3Converter.java index 43facd7d..b7c41d84 100644 --- a/src/main/java/org/spdx/library/conversion/Spdx2to3Converter.java +++ b/src/main/java/org/spdx/library/conversion/Spdx2to3Converter.java @@ -700,17 +700,18 @@ public SpdxDocument convertAndStore(org.spdx.library.model.v2.SpdxDocument fromD } ).collect(Collectors.toList())); for (org.spdx.library.model.v2.license.ExtractedLicenseInfo extractedLicense:fromDoc.getExtractedLicenseInfos()) { - convertAndStore(extractedLicense); + toDoc.getRootElements().add(convertAndStore(extractedLicense)); } convertOrphanElements(fromDoc, toDoc); return toDoc; } /** - * Converts any SPDX spec version 2 packages, files, and snippets belonging to fromDoc which are not - * reachable through documentDescribes or a relationship (i.e. have no edges connecting them to anything - * else in the document). Without this, such elements would be silently dropped from the conversion since - * they would never be visited by the relationship graph traversal performed elsewhere in this class. + * Converts any SPDX spec version 2 packages, files, snippets, listed licenses, and listed license + * exceptions belonging to fromDoc which are not reachable through documentDescribes, a relationship, + * or a license expression (i.e. have no edges connecting them to anything else in the document). + * Without this, such elements would be silently dropped from the conversion since they would never + * be visited by the relationship graph traversal performed elsewhere in this class. * Any such elements found are converted, stored, and added to the toDoc's root elements. * @param fromDoc SPDX spec version 2 document being converted from * @param toDoc SPDX spec version 3 document being converted to @@ -731,6 +732,24 @@ private void convertOrphanElements(org.spdx.library.model.v2.SpdxDocument fromDo } } } + // Listed licenses and listed license exceptions are stored under the SPDX License List namespace + // rather than the document's namespace, so no object URI prefix filter is used to find them. + List fromListedLicenses = ((Stream) + SpdxModelFactory.getSpdxObjects(fromModelStore, copyManager, SpdxConstantsCompatV2.CLASS_SPDX_LISTED_LICENSE, null, null)) + .collect(Collectors.toList()); + for (org.spdx.library.model.v2.license.SpdxListedLicense fromListedLicense : fromListedLicenses) { + if (!alreadyCopied(fromListedLicense.getObjectUri())) { + toDoc.getRootElements().add(convertAndStore(fromListedLicense)); + } + } + List fromListedExceptions = ((Stream) + SpdxModelFactory.getSpdxObjects(fromModelStore, copyManager, SpdxConstantsCompatV2.CLASS_SPDX_LISTED_LICENSE_EXCEPTION, null, null)) + .collect(Collectors.toList()); + for (org.spdx.library.model.v2.license.ListedLicenseException fromListedException : fromListedExceptions) { + if (!alreadyCopied(fromListedException.getObjectUri())) { + toDoc.getRootElements().add(convertAndStore(fromListedException)); + } + } } /** diff --git a/src/test/java/org/spdx/library/conversion/Spdx2to3ConverterTest.java b/src/test/java/org/spdx/library/conversion/Spdx2to3ConverterTest.java index 3fb32ca6..a646e183 100644 --- a/src/test/java/org/spdx/library/conversion/Spdx2to3ConverterTest.java +++ b/src/test/java/org/spdx/library/conversion/Spdx2to3ConverterTest.java @@ -608,9 +608,21 @@ public void testConvertAndStoreSpdxDocument() throws InvalidSPDXAnalysisExceptio assertEquals(annotationComment, resultAnnotation.getStatement().get()); Element[] rootElements = result.getRootElements().toArray(new Element[result.getRootElements().size()]); - assertEquals(1, rootElements.length); - assertTrue(rootElements[0] instanceof SpdxPackage); - SpdxPackage resultPkg = (SpdxPackage)rootElements[0]; + assertEquals(2, rootElements.length); + SpdxPackage resultPkg = null; + CustomLicense rootCustomLicense = null; + for (Element rootElement:rootElements) { + if (rootElement instanceof SpdxPackage) { + resultPkg = (SpdxPackage)rootElement; + } else if (rootElement instanceof CustomLicense) { + rootCustomLicense = (CustomLicense)rootElement; + } else { + fail("Unexpected root element type "+rootElement.getClass().getName()); + } + } + assertTrue(Objects.nonNull(resultPkg)); + assertTrue(Objects.nonNull(rootCustomLicense)); + assertEquals(extractedLicName, rootCustomLicense.getName().get()); assertEquals(pkgName, resultPkg.getName().get()); assertEquals(pkgDownloadLocation, resultPkg.getDownloadLocation().get()); assertEquals(pkgCopyright, resultPkg.getCopyrightText().get()); @@ -722,6 +734,85 @@ public void testConvertAndStoreSpdxDocumentOrphanPackage() throws InvalidSPDXAna assertTrue(resultPackageNames.contains(describedPkgName)); assertTrue(resultPackageNames.contains(orphanPkgName)); } + + /** + * Test method for {@link org.spdx.library.conversion.Spdx2to3Converter#convertAndStore(org.spdx.library.model.v2.SpdxDocument)}. + * Verifies that extracted licensing info, listed licenses, and listed license exceptions stored in the + * document's model store but not referenced by any package, file, or snippet are still converted and + * added as root elements rather than silently dropped. + * @throws InvalidSPDXAnalysisException + */ + @Test + public void testConvertAndStoreSpdxDocumentOrphanLicenses() throws InvalidSPDXAnalysisException { + String pkgName = "package"; + String extractedLicName = "Extracted License Name"; + String extractedLicText = "Extracted license text"; + String orphanLicenseId = "Apache-2.0"; + String orphanExceptionId = "Classpath-exception-2.0"; + + org.spdx.library.model.v2.SpdxDocument doc = new org.spdx.library.model.v2.SpdxDocument(fromModelStore, DOCUMENT_URI, copyManager, true); + doc.setCreationInfo(doc.createCreationInfo(Arrays.asList(new String[] {SpdxConstantsCompatV2.CREATOR_PREFIX_TOOL + "test"}), + "2010-01-29T18:30:22Z")); + org.spdx.library.model.v2.license.AnyLicenseInfo dataLicense = + LicenseInfoFactory.parseSPDXLicenseStringCompatV2("CC0-1.0", fromModelStore, DOCUMENT_URI, copyManager); + doc.setDataLicense(dataLicense); + + org.spdx.library.model.v2.license.AnyLicenseInfo noAssertion = + new org.spdx.library.model.v2.license.SpdxNoAssertionLicense(); + org.spdx.library.model.v2.SpdxPackage pkg = doc.createPackage(fromModelStore.getNextId(IdType.SpdxId), + pkgName, noAssertion, "copyright", noAssertion) + .setFilesAnalyzed(false) + .setDownloadLocation("NOASSERTION") + .build(); + doc.setDocumentDescribes(Arrays.asList(new org.spdx.library.model.v2.SpdxItem[] {pkg})); + + // extracted license info stored at the document level but not referenced by the package + org.spdx.library.model.v2.license.ExtractedLicenseInfo extractedLicense = + new org.spdx.library.model.v2.license.ExtractedLicenseInfo(fromModelStore, DOCUMENT_URI, + fromModelStore.getNextId(IdType.LicenseRef), copyManager, true); + extractedLicense.setExtractedText(extractedLicText); + extractedLicense.setName(extractedLicName); + doc.setExtractedLicenseInfos(Arrays.asList(new org.spdx.library.model.v2.license.ExtractedLicenseInfo[] {extractedLicense})); + + // listed license and listed license exception stored in the document's model store but not + // referenced by the package, a relationship, or documentDescribes + org.spdx.library.model.v2.license.SpdxListedLicense orphanLicense = + new org.spdx.library.model.v2.license.SpdxListedLicense(fromModelStore, + SpdxConstantsCompatV2.LISTED_LICENSE_NAMESPACE_PREFIX, orphanLicenseId, copyManager, true); + org.spdx.library.model.v2.license.ListedLicenseException orphanException = + new org.spdx.library.model.v2.license.ListedLicenseException(fromModelStore, + SpdxConstantsCompatV2.LISTED_LICENSE_NAMESPACE_PREFIX, orphanExceptionId, copyManager, true); + + Spdx2to3Converter converter = new Spdx2to3Converter(toModelStore, copyManager, defaultCreationInfo, + SpdxModelFactory.getLatestSpecVersion(), DEFAULT_PREFIX, true); + SpdxDocument result = converter.convertAndStore(doc); + + Element[] rootElements = result.getRootElements().toArray(new Element[result.getRootElements().size()]); + assertEquals(4, rootElements.length); + boolean foundPkg = false; + boolean foundCustomLicense = false; + boolean foundListedLicense = false; + boolean foundListedException = false; + for (Element rootElement:rootElements) { + if (rootElement instanceof SpdxPackage) { + foundPkg = true; + assertEquals(pkgName, ((SpdxPackage)rootElement).getName().get()); + } else if (rootElement instanceof CustomLicense) { + foundCustomLicense = true; + assertEquals(extractedLicName, ((CustomLicense)rootElement).getName().get()); + } else if (rootElement instanceof ListedLicense) { + foundListedLicense = true; + } else if (rootElement instanceof ListedLicenseException) { + foundListedException = true; + } else { + fail("Unexpected root element type "+rootElement.getClass().getName()); + } + } + assertTrue(foundPkg); + assertTrue(foundCustomLicense); + assertTrue(foundListedLicense); + assertTrue(foundListedException); + } @Test public void testConvertAndStoreExternalDocRef() throws InvalidSPDXAnalysisException {