From 44544ff52aa8903f388a906c78930423b4eeed76 Mon Sep 17 00:00:00 2001 From: Chao Wang Date: Fri, 13 Mar 2026 11:04:07 +0800 Subject: [PATCH 1/2] fix: handle missing [workspace.dependencies] in virtual workspace component analysis (#344) Add null check for workspaceDepsTable in processWorkspaceDependencies to prevent NPE when a virtual workspace Cargo.toml has [workspace] with members but no [workspace.dependencies] section. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../trustifyda/providers/CargoProvider.java | 6 ++ .../CargoProviderCargoParsingTest.java | 64 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java index 52ca76d4..7f07ce16 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java @@ -216,6 +216,12 @@ private void processWorkspaceDependencies( TomlParseResult tomlResult) { var workspaceDepsTable = tomlResult.getTable("workspace.dependencies"); + if (workspaceDepsTable == null) { + if (debugLoggingIsNeeded()) { + log.info("No [workspace.dependencies] section found, skipping workspace dependencies"); + } + return; + } if (debugLoggingIsNeeded()) { log.info("Processing " + workspaceDepsTable.keySet().size() + " workspace dependencies"); } diff --git a/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderCargoParsingTest.java b/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderCargoParsingTest.java index 72b36545..1c54aac5 100644 --- a/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderCargoParsingTest.java +++ b/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderCargoParsingTest.java @@ -19,16 +19,27 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.github.packageurl.PackageURL; +import io.github.guacsec.trustifyda.sbom.Sbom; +import io.github.guacsec.trustifyda.sbom.SbomFactory; +import io.github.guacsec.trustifyda.tools.Ecosystem.Type; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.tomlj.Toml; +import org.tomlj.TomlParseResult; public class CargoProviderCargoParsingTest { @@ -720,4 +731,57 @@ public void testVirtualRootWithoutVersionUsesDefault(@TempDir Path tempDir) thro assertTrue(stackSbom.contains("1.0.0"), "SBOM should contain default version: 1.0.0"); } + + @Test + public void testVirtualWorkspaceWithoutWorkspaceDepsDoesNotThrowNPE(@TempDir Path tempDir) + throws Exception { + // Create a Cargo.toml with [workspace] members but NO [workspace.dependencies] + Path cargoToml = tempDir.resolve("Cargo.toml"); + String content = + """ + [workspace] + members = ["crate-a", "crate-b"] + + [workspace.package] + version = "1.0.0" + edition = "2021" + """; + Files.writeString(cargoToml, content); + + TomlParseResult tomlResult = Toml.parse(cargoToml); + // Verify precondition: workspace.dependencies table is null + assertNull( + tomlResult.getTable("workspace.dependencies"), + "Precondition: workspace.dependencies table should be null"); + + CargoProvider provider = new CargoProvider(cargoToml); + Sbom sbom = SbomFactory.newInstance(); + PackageURL root = + new PackageURL(Type.CARGO.getType(), null, "test-workspace", "1.0.0", null, null); + sbom.addRoot(root); + + // Call the private processWorkspaceDependencies via reflection + var method = + CargoProvider.class.getDeclaredMethod( + "processWorkspaceDependencies", + Sbom.class, + PackageURL.class, + Map.class, + Set.class, + TomlParseResult.class); + method.setAccessible(true); + + // This should NOT throw NPE when [workspace.dependencies] is absent + try { + method.invoke(provider, sbom, root, new HashMap<>(), new HashSet<>(), tomlResult); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof NullPointerException) { + throw new AssertionError( + "processWorkspaceDependencies threw NPE when [workspace.dependencies] is absent." + + " The method should handle a missing workspace.dependencies table gracefully.", + e.getCause()); + } + throw e; + } + } } From 3b58a1acd9ca6f2a481e89a64f7a4e15e78276a4 Mon Sep 17 00:00:00 2001 From: Chao Wang Date: Fri, 13 Mar 2026 15:57:53 +0800 Subject: [PATCH 2/2] fix: replace reflection with package-private test seam in CargoProvider test --- .../trustifyda/providers/CargoProvider.java | 2 +- .../CargoProviderCargoParsingTest.java | 30 ++++--------------- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java b/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java index 7f07ce16..19cfc636 100644 --- a/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java +++ b/src/main/java/io/github/guacsec/trustifyda/providers/CargoProvider.java @@ -208,7 +208,7 @@ private void handleVirtualWorkspace( } } - private void processWorkspaceDependencies( + void processWorkspaceDependencies( Sbom sbom, PackageURL root, Map packageMap, diff --git a/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderCargoParsingTest.java b/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderCargoParsingTest.java index 1c54aac5..1d3c922c 100644 --- a/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderCargoParsingTest.java +++ b/src/test/java/io/github/guacsec/trustifyda/providers/CargoProviderCargoParsingTest.java @@ -16,6 +16,7 @@ */ package io.github.guacsec.trustifyda.providers; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -28,13 +29,11 @@ import io.github.guacsec.trustifyda.sbom.SbomFactory; import io.github.guacsec.trustifyda.tools.Ecosystem.Type; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -760,28 +759,11 @@ public void testVirtualWorkspaceWithoutWorkspaceDepsDoesNotThrowNPE(@TempDir Pat new PackageURL(Type.CARGO.getType(), null, "test-workspace", "1.0.0", null, null); sbom.addRoot(root); - // Call the private processWorkspaceDependencies via reflection - var method = - CargoProvider.class.getDeclaredMethod( - "processWorkspaceDependencies", - Sbom.class, - PackageURL.class, - Map.class, - Set.class, - TomlParseResult.class); - method.setAccessible(true); - // This should NOT throw NPE when [workspace.dependencies] is absent - try { - method.invoke(provider, sbom, root, new HashMap<>(), new HashSet<>(), tomlResult); - } catch (InvocationTargetException e) { - if (e.getCause() instanceof NullPointerException) { - throw new AssertionError( - "processWorkspaceDependencies threw NPE when [workspace.dependencies] is absent." - + " The method should handle a missing workspace.dependencies table gracefully.", - e.getCause()); - } - throw e; - } + assertDoesNotThrow( + () -> + provider.processWorkspaceDependencies( + sbom, root, new HashMap<>(), new HashSet<>(), tomlResult), + "processWorkspaceDependencies should handle missing [workspace.dependencies] gracefully"); } }