From cce810ed3086ae8bc2f6b94aa83ab2afe1374efc Mon Sep 17 00:00:00 2001 From: Xueming Shen Date: Fri, 5 Jun 2026 17:44:55 +0000 Subject: [PATCH] 8385355: NullPointerException in jdk.tools.jlink.internal.ImageResourcesTree after JDK-8377070 Reviewed-by: alanb Backport-of: 92298786486daf092c8eb8f278818441df1f6b51 --- .../jlink/internal/ImageResourcesTree.java | 40 +++++++++++++++---- .../jdk/internal/jimage/ImageReaderTest.java | 32 ++++++++++++++- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageResourcesTree.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageResourcesTree.java index 2f8143820ca..ab0809348ca 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageResourcesTree.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageResourcesTree.java @@ -235,22 +235,46 @@ public final class ImageResourcesTree { return; } String modName = fullPath.substring(1, modEnd); - String pkgPath = fullPath.substring(modEnd + 1); + String resPath = fullPath.substring(modEnd + 1); + int pathEnd = resPath.lastIndexOf('/'); Node parentNode = getDirectoryNode(modName, modulesRoot); boolean isPreviewPath = false; - if (pkgPath.startsWith(PREVIEW_PREFIX)) { + if (resPath.startsWith("META-INF/")) { + parentNode = getDirectoryNode("META-INF", parentNode); + if (!resPath.startsWith(PREVIEW_PREFIX)) { + // Non-preview META-INF paths are resources, not package + // hierarchies, so directory and file names may contain dots. + for (int i = "META-INF".length(), j; i != pathEnd; i = j) { + j = resPath.indexOf('/', i + 1); + parentNode = getDirectoryNode(resPath.substring(i + 1, j), parentNode); + } + String resourceName = resPath.substring(pathEnd + 1); + Node resourceNode = parentNode.getChildren(resourceName); + if (resourceNode == null) { + new ResourceNode(resourceName, parentNode); + } else if (!(resourceNode instanceof ResourceNode)) { + throw new InvalidTreeException(resourceNode); + } + return; + } // For preview paths, process nodes relative to the preview directory. - pkgPath = pkgPath.substring(PREVIEW_PREFIX.length()); - Node metaInf = getDirectoryNode("META-INF", parentNode); - parentNode = getDirectoryNode("preview", metaInf); + parentNode = getDirectoryNode("preview", parentNode); + resPath = resPath.substring(PREVIEW_PREFIX.length()); + pathEnd -= PREVIEW_PREFIX.length(); isPreviewPath = true; } - int pathEnd = pkgPath.lastIndexOf('/'); // From invariants tested above, this must now be well-formed. - String fullPkgName = (pathEnd == -1) ? "" : pkgPath.substring(0, pathEnd).replace('/', '.'); - String resourceName = pkgPath.substring(pathEnd + 1); + String pkgPath = (pathEnd == -1) ? "" : resPath.substring(0, pathEnd); + if (pkgPath.contains(".")) { + // Non META-INF entries are package paths. Dots in path segment + // names would otherwise be confused with package separators. + System.err.println("Invalid package path, skipping " + pkgPath); + return; + } + String fullPkgName = pkgPath.replace('/', '.'); + String resourceName = resPath.substring(pathEnd + 1); // Intermediate packages are marked "empty" (no resources). This might // later be merged with a non-empty link for the same package. ModuleLink emptyLink = ModuleLink.forEmptyPackage(modName, isPreviewPath); diff --git a/test/jdk/jdk/internal/jimage/ImageReaderTest.java b/test/jdk/jdk/internal/jimage/ImageReaderTest.java index b8f7763e97e..5104bb97f95 100644 --- a/test/jdk/jdk/internal/jimage/ImageReaderTest.java +++ b/test/jdk/jdk/internal/jimage/ImageReaderTest.java @@ -37,6 +37,7 @@ import tests.Helper; import tests.JImageGenerator; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Arrays; import java.util.List; @@ -58,6 +59,7 @@ import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; /* * @test + * @bug 8385355 * @summary Tests for ImageReader. * @modules java.base/jdk.internal.jimage * jdk.jlink/jdk.tools.jlink.internal @@ -72,13 +74,18 @@ import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; /// There is no mutable test instance state to worry about. @TestInstance(PER_CLASS) public class ImageReaderTest { - // The '@' prefix marks the entry as a preview entry which will be placed in - // the '/modules//META-INF/preview/...' namespace. + // The '@' prefix marks the entry as a preview class entry which will be placed in + // the '/modules//META-INF/preview/...' namespace. The '!' prefix marks + // the entry as a non-class resource path. private static final Map> IMAGE_ENTRIES = Map.of( "modfoo", Arrays.asList( "com.foo.HasPreviewVersion", "com.foo.NormalFoo", "com.foo.bar.NormalBar", + "!META-INF/maven/com.google.code.findbugs/jsr305/pom.properties", + "!META-INF/z", + "!META-INF/collision/child.properties", + "!META-INF/collision", // Replaces original class in preview mode. "@com.foo.HasPreviewVersion", // New class in existing package in preview mode. @@ -138,6 +145,23 @@ public class ImageReaderTest { } } + @Test + public void testMetaInfResourcesAreNotPackagePaths() throws IOException { + for (PreviewMode mode : List.of(PreviewMode.ENABLED, PreviewMode.DISABLED)) { + try (ImageReader reader = ImageReader.open(image, mode)) { + assertResource(reader, "modfoo", "META-INF/maven/com.google.code.findbugs/jsr305/pom.properties"); + assertResource(reader, "modfoo", "META-INF/z"); + assertResource(reader, "modfoo", "META-INF/collision/child.properties"); + assertDirContents(reader, "/modules/modfoo/META-INF", "MANIFEST.MF", "collision", "maven", "z"); + assertDirContents(reader, "/modules/modfoo/META-INF/collision", "child.properties"); + assertDirContents(reader, "/modules/modfoo/META-INF/maven", "com.google.code.findbugs"); + assertDirContents(reader, "/modules/modfoo/META-INF/maven/com.google.code.findbugs", "jsr305"); + assertDirContents(reader, "/modules/modfoo/META-INF/maven/com.google.code.findbugs/jsr305", "pom.properties"); + assertAbsent(reader, "/modules/modfoo/META-INF/maven/com/google/code/findbugs/jsr305/pom.properties"); + } + } + } + @ParameterizedTest @CsvSource(delimiter = ':', value = { "modfoo:com/foo/HasPreviewVersion.class", @@ -416,6 +440,10 @@ public class ImageReaderTest { jar.addEntry("module-info.class", InMemoryJavaCompiler.compile("module-info", moduleInfo)); classes.forEach(fqn -> { + if (fqn.startsWith("!")) { + jar.addEntry(fqn.substring(1), "resource".getBytes(StandardCharsets.UTF_8)); + return; + } boolean isPreviewEntry = fqn.startsWith("@"); if (isPreviewEntry) { fqn = fqn.substring(1);