From c042ad289d10e4c2dba4371630ff82318b0aa23b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Du=EF=BF=BDan=20B=EF=BF=BDlek?= Date: Thu, 2 Jul 2026 09:32:44 +0000 Subject: [PATCH] 8386842: Preview files in root directory not recognized in system image Reviewed-by: alanb, sherman, liach --- .../jdk/internal/jimage/ImageReader.java | 48 +++++++++++++++-- .../jdk/internal/jimage/ImageReaderTest.java | 52 ++++++++++++++++++- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java index 2cf28b835ce..59c2392dea9 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java @@ -486,7 +486,7 @@ public final class ImageReader implements AutoCloseable { ImageLocation loc = null; if (isPreviewEnabled) { // We must test preview location first (if in preview mode). - loc = findLocation(moduleName, PREVIEW_RESOURCE_PREFIX + resourcePath); + loc = findLocation(moduleName, PREVIEW_RESOURCE_PREFIX + "/" + resourcePath); } if (loc == null) { loc = findLocation(moduleName, resourcePath); @@ -531,7 +531,7 @@ public final class ImageReader implements AutoCloseable { return node.isResource(); } } - loc = findLocation(moduleName, PREVIEW_RESOURCE_PREFIX + resourcePath); + loc = findLocation(moduleName, PREVIEW_RESOURCE_PREFIX + "/" + resourcePath); } if (loc == null) { loc = findLocation(moduleName, resourcePath); @@ -561,7 +561,19 @@ public final class ImageReader implements AutoCloseable { // Now try the non-prefixed resource name, but be careful to avoid false // positives for names like "/modules/modules/xxx" which could return a // location of a directory entry. - loc = findLocation(name.substring(MODULES_PREFIX.length())); + String resourceName = name.substring(MODULES_PREFIX.length()); + if (isPreviewEnabled) { + // Root-level preview resources are not pre-cached when an image + // is opened, so check for them first. + int pathStart = resourceName.indexOf('/', 1); + if (pathStart > 1 && resourceName.indexOf('/', pathStart + 1) < 0) { + loc = findLocation(resourceName.substring(0, pathStart) + + PREVIEW_INFIX + "/" + resourceName.substring(pathStart + 1)); + } + } + if (loc == null) { + loc = findLocation(resourceName); + } return loc != null && loc.getType() == RESOURCE ? ensureCached(newResource(name, loc)) : null; @@ -649,6 +661,36 @@ public final class ImageReader implements AutoCloseable { private Directory completeModuleDirectory(Directory dir, ImageLocation loc) { assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir; List previewOnlyNodes = getPreviewNodesToMerge(dir); + if (isPreviewEnabled && previewOnlyNodes.isEmpty()) { + // When opening an image in preview mode, packages that have preview + // content are eagerly processed, caching preview resources and + // preview-only directories for direct lookup. Root-level preview + // resources are omitted during this process, since they have no + // package path and the empty package is not represented under + // "/packages", and must be processed separately. + int moduleStart = MODULES_PREFIX.length() + 1; + if (dir.getName().indexOf('/', moduleStart) < 0) { + ImageLocation previewLoc = findLocation(dir.getName() + PREVIEW_INFIX); + if (previewLoc != null) { + previewOnlyNodes = createChildNodes(previewLoc, 0, childLoc -> { + String baseName = getBaseName(childLoc); + String nonPreviewChildName = dir.getName() + "/" + baseName; + boolean isPreviewOnly = ImageLocation.isPreviewOnly(childLoc.getFlags()); + LocationType type = childLoc.getType(); + if (type == RESOURCE) { + Node childNode = nodes.computeIfAbsent(nonPreviewChildName, n -> newResource(n, childLoc)); + return isPreviewOnly ? childNode : null; + } else { + assert type == MODULES_DIR : "Invalid location type: " + childLoc; + Node childNode = nodes.get(nonPreviewChildName); + assert !(isPreviewOnly && childNode == null) : + "Inconsistent child node: " + nonPreviewChildName; + return isPreviewOnly ? childNode : null; + } + }); + } + } + } // We hide preview names from direct lookup, but must also prevent // the preview directory from appearing in any META-INF directories. boolean parentIsMetaInfDir = isMetaInf(dir); diff --git a/test/jdk/jdk/internal/jimage/ImageReaderTest.java b/test/jdk/jdk/internal/jimage/ImageReaderTest.java index 5104bb97f95..0fadc6eec12 100644 --- a/test/jdk/jdk/internal/jimage/ImageReaderTest.java +++ b/test/jdk/jdk/internal/jimage/ImageReaderTest.java @@ -46,6 +46,7 @@ import java.util.Set; import java.util.stream.Collectors; import static java.util.stream.Collectors.toSet; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -59,7 +60,7 @@ import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; /* * @test - * @bug 8385355 + * @bug 8385355 8386842 * @summary Tests for ImageReader. * @modules java.base/jdk.internal.jimage * jdk.jlink/jdk.tools.jlink.internal @@ -86,6 +87,11 @@ public class ImageReaderTest { "!META-INF/z", "!META-INF/collision/child.properties", "!META-INF/collision", + // Non-class resource in top level directory + "!fileA.txt", + "!fileB.txt", + "!META-INF/preview/fileA.txt", + "!META-INF/preview/fileB.txt", // Replaces original class in preview mode. "@com.foo.HasPreviewVersion", // New class in existing package in preview mode. @@ -96,6 +102,11 @@ public class ImageReaderTest { // Two new packages in preview mode (new symbolic links). "@com.bar.preview.stuff.Foo", "@com.bar.preview.stuff.Bar"), + "modbaz", Arrays.asList( + "!file.txt", + "!normal.txt", + "!META-INF/preview/file.txt", + "!META-INF/preview/previewOnly.txt"), "modgus", Arrays.asList( // A second module with a preview-only empty package (preview). "@com.bar.preview.other.Gus")); @@ -270,6 +281,18 @@ public class ImageReaderTest { assertAbsent(reader, "/modules/modfoo/com/foo/bar/IsPreviewOnly.class"); assertDirContents(reader, "/modules/modfoo/com/foo", "HasPreviewVersion.class", "NormalFoo.class", "bar"); assertDirContents(reader, "/modules/modfoo/com/foo/bar", "NormalBar.class"); + + // Non-class resource in top level directory + assertResource(reader, "modfoo", "fileA.txt"); + assertNonPreviewResourceVersion(reader, "modfoo", "fileA.txt"); + assertNode(reader, "/modules/modfoo/fileB.txt"); + assertNonPreviewResourceVersion(reader, "modfoo", "fileB.txt"); + assertDirContents(reader, "/modules/modfoo", "META-INF", "module-info.class", "fileA.txt", "fileB.txt", "com"); + + assertAbsent(reader, "/modules/modbaz/previewOnly.txt"); + assertDirContents(reader, "/modules/modbaz", "META-INF", "module-info.class", "file.txt", "normal.txt"); + assertNonPreviewResourceVersion(reader, "modbaz", "file.txt"); + assertNonPreviewResourceVersion(reader, "modbaz", "normal.txt"); } } @@ -289,6 +312,20 @@ public class ImageReaderTest { assertResource(reader, "modfoo", "com/foo/bar/IsPreviewOnly.class"); assertDirContents(reader, "/modules/modfoo/com/foo", "HasPreviewVersion.class", "NormalFoo.class", "bar"); assertDirContents(reader, "/modules/modfoo/com/foo/bar", "NormalBar.class", "IsPreviewOnly.class"); + + // Non-class resource in top level directory + assertResource(reader, "modfoo", "fileA.txt"); + assertPreviewResourceVersion(reader, "modfoo", "fileA.txt"); + assertNode(reader, "/modules/modfoo/fileB.txt"); + assertPreviewResourceVersion(reader, "modfoo", "fileB.txt"); + assertDirContents(reader, "/modules/modfoo/com", "foo"); + assertDirContents(reader, "/modules/modfoo", "META-INF", "module-info.class", "fileA.txt", "fileB.txt", "com"); + + assertNode(reader, "/modules/modbaz/previewOnly.txt"); + assertDirContents(reader, "/modules/modbaz", "META-INF", "module-info.class", "file.txt", "normal.txt", "previewOnly.txt"); + assertPreviewResourceVersion(reader, "modbaz", "file.txt"); + assertNonPreviewResourceVersion(reader, "modbaz", "normal.txt"); + assertPreviewResourceVersion(reader, "modbaz", "previewOnly.txt"); } } @@ -405,6 +442,17 @@ public class ImageReaderTest { assertSame(resNode, reader.findNode(nodeName)); } + private static void assertNonPreviewResourceVersion(ImageReader reader, String modName, String resPath) throws IOException { + Node resNode = reader.findResourceNode(modName, resPath); + assertArrayEquals(resPath.getBytes(StandardCharsets.UTF_8), reader.getResource(resNode)); + } + + private static void assertPreviewResourceVersion(ImageReader reader, String modName, String resPath) throws IOException { + Node resNode = reader.findResourceNode(modName, resPath); + String name = "META-INF/preview/" + resPath; + assertArrayEquals(name.getBytes(StandardCharsets.UTF_8), reader.getResource(resNode)); + } + private static void assertNonPreviewVersion(ImageClassLoader loader, String module, String fqn) throws IOException { assertEquals("Class: " + fqn, loader.loadAndGetToString(module, fqn)); } @@ -441,7 +489,7 @@ public class ImageReaderTest { classes.forEach(fqn -> { if (fqn.startsWith("!")) { - jar.addEntry(fqn.substring(1), "resource".getBytes(StandardCharsets.UTF_8)); + jar.addEntry(fqn.substring(1), fqn.substring(1).getBytes(StandardCharsets.UTF_8)); return; } boolean isPreviewEntry = fqn.startsWith("@");