diff --git a/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java index c06c62488db..24dd8ea3121 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java @@ -37,8 +37,12 @@ import java.nio.file.StandardOpenOption; import java.security.AccessController; import java.security.PrivilegedAction; +import java.util.NoSuchElementException; import java.util.Objects; +import java.util.Random; import java.util.stream.IntStream; +import java.util.stream.Stream; + import jdk.internal.jimage.decompressor.Decompressor; /** @@ -326,6 +330,56 @@ public String[] getEntryNames() { .toArray(String[]::new); } + /** + * Returns the "raw" API for accessing underlying jimage resource entries. + * + *

This is only meaningful for use by code dealing directly with jimage + * files, and cannot be used to reliably lookup resources used at runtime. + * + *

This API remains valid until the image reader from which it was + * obtained is closed. + */ + // Package visible for use by ImageReader. + ResourceEntries getResourceEntries() { + return new ResourceEntries() { + @Override + public Stream getEntryNames(String module) { + if (module.isEmpty() || module.equals("modules") || module.equals("packages")) { + throw new IllegalArgumentException("Invalid module name: " + module); + } + return IntStream.range(0, offsets.capacity()) + .map(offsets::get) + .filter(offset -> offset != 0) + // Reusing a location instance or getting the module + // offset directly would save a lot of allocations here. + .mapToObj(offset -> ImageLocation.readFrom(BasicImageReader.this, offset)) + // Reverse lookup of module offset would be faster here. + .filter(loc -> module.equals(loc.getModule())) + .map(ImageLocation::getFullName); + } + + private ImageLocation getResourceLocation(String name) { + if (!name.startsWith("/modules/") && !name.startsWith("/packages/")) { + ImageLocation location = BasicImageReader.this.findLocation(name); + if (location != null) { + return location; + } + } + throw new NoSuchElementException("No such resource entry: " + name); + } + + @Override + public long getSize(String name) { + return getResourceLocation(name).getUncompressedSize(); + } + + @Override + public byte[] getBytes(String name) { + return BasicImageReader.this.getResource(getResourceLocation(name)); + } + }; + } + ImageLocation getLocation(int offset) { return ImageLocation.readFrom(this, offset); } 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 a8ab2ff465b..323561eab00 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java @@ -230,6 +230,11 @@ public ByteBuffer getResourceBuffer(Node node) { return reader.getResourceBuffer(node.getLocation()); } + // Package protected for use only by SystemImageReader. + ResourceEntries getResourceEntries() { + return reader.getResourceEntries(); + } + private static final class SharedImageReader extends BasicImageReader { // There are >30,000 nodes in a complete jimage tree, and even relatively // common tasks (e.g. starting up javac) load somewhere in the region of diff --git a/src/java.base/share/classes/jdk/internal/jimage/ResourceEntries.java b/src/java.base/share/classes/jdk/internal/jimage/ResourceEntries.java new file mode 100644 index 00000000000..25f5d484e71 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/jimage/ResourceEntries.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.internal.jimage; + +import java.io.InputStream; +import java.util.stream.Stream; + +/** + * Accesses the underlying resource entries in a jimage file. + * + *

This API is designed only for use by the jlink classes, which read the raw + * jimage files. Use the {@link ImageReader} API to read jimage contents at + * runtime to correctly account for preview mode. + * + *

This API ignores the {@code previewMode} of the {@link ImageReader} from + * which it is obtained, and returns an unmapped view of entries (e.g. allowing + * for direct access of resources in the {@code META-INF/preview/...} namespace). + * + *

It disallows access to resource directories (i.e. {@code "/modules/..."}) + * or packages entries (i.e. {@code "/packages/..."}). + * + * @implNote This class needs to maintain JDK 8 source compatibility. + * + * It is used internally in the JDK to implement jimage/jrtfs access, + * but also compiled and delivered as part of the jrtfs.jar to support access + * to the jimage file provided by the shipped JDK by tools running on JDK 8. + */ +public interface ResourceEntries { + /** + * Returns the jimage names for all resources in the given module, in + * random order. Entry names will always be prefixed by the given module + * name (e.g. {@code "//..."}). + */ + Stream getEntryNames(String module); + + /** + * Returns the (uncompressed) size of a resource given its jimage name. + * + * @throws java.util.NoSuchElementException if the resource does not exist. + */ + long getSize(String name); + + /** + * Returns a copy of a resource's content given its jimage name. + * + * @throws java.util.NoSuchElementException if the resource does not exist. + */ + byte[] getBytes(String name); +} diff --git a/src/java.base/share/classes/jdk/internal/jimage/SystemImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/SystemImageReader.java index 38bf786e533..e5ed3d13fd8 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/SystemImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/SystemImageReader.java @@ -73,5 +73,15 @@ public static ImageReader get() { return SYSTEM_IMAGE_READER; } + /** + * Returns the "raw" API for accessing underlying jimage resource entries. + * + *

This is only meaningful for use by code dealing directly with jimage + * files, and cannot be used to reliably lookup resources used at runtime. + */ + public static ResourceEntries getResourceEntries() { + return get().getResourceEntries(); + } + private SystemImageReader() {} } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Archive.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Archive.java index c56346b6994..29f1e4173f4 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Archive.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Archive.java @@ -24,6 +24,7 @@ */ package jdk.tools.jlink.internal; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; @@ -34,7 +35,7 @@ * An Archive of all content, classes, resources, configuration files, and * other, for a module. */ -public interface Archive { +public interface Archive extends Closeable { /** * Entry is contained in an Archive @@ -59,11 +60,12 @@ public static enum EntryType { private final String path; /** - * Constructs an entry of the given archive - * @param archive archive - * @param path - * @param name an entry name that does not contain the module name - * @param type + * Constructs an entry of the given archive. + * + * @param archive the archive in which this entry exists. + * @param path the complete path of the entry, including the module. + * @param name an entry name relative to its containing module. + * @param type the entry type. */ public Entry(Archive archive, String path, String name, EntryType type) { this.archive = Objects.requireNonNull(archive); @@ -72,10 +74,6 @@ public Entry(Archive archive, String path, String name, EntryType type) { this.type = Objects.requireNonNull(type); } - public final Archive archive() { - return archive; - } - public final EntryType type() { return type; } @@ -134,5 +132,6 @@ public String toString() { /* * Close the archive */ + @Override void close() throws IOException; } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java index aac220e5b94..f30fe9a14d9 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2024, Red Hat, Inc. + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,7 +33,6 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.lang.module.ModuleFinder; -import java.lang.module.ModuleReference; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -51,6 +51,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import jdk.internal.jimage.ResourceEntries; +import jdk.internal.jimage.SystemImageReader; import jdk.internal.util.OperatingSystem; import jdk.tools.jlink.internal.Archive.Entry.EntryType; import jdk.tools.jlink.internal.runtimelink.ResourceDiff; @@ -63,10 +65,9 @@ * associated files from the filesystem of the JDK installation. */ public class JRTArchive implements Archive { - private final String module; private final Path path; - private final ModuleReference ref; + private final ResourceEntries imageResources; // The collection of files of this module private final List files = new ArrayList<>(); // Files not part of the lib/modules image of the JDK install. @@ -99,12 +100,11 @@ public class JRTArchive implements Archive { Set upgradeableFiles) { this.module = module; this.path = path; - this.ref = ModuleFinder.ofSystem() - .find(module) - .orElseThrow(() -> - new IllegalArgumentException( - "Module " + module + - " not part of the JDK install")); + ModuleFinder.ofSystem() + .find(module) + .orElseThrow(() -> new IllegalArgumentException( + "Module " + module + " not part of the JDK install")); + this.imageResources = SystemImageReader.getResourceEntries(); this.errorOnModifiedFile = errorOnModifiedFile; this.otherRes = readModuleResourceFile(module); this.resDiff = Objects.requireNonNull(perModDiff).stream() @@ -159,52 +159,35 @@ public boolean equals(Object obj) { Objects.equals(path, other.path)); } + private boolean isNormalOrModifiedDiff(String name) { + ResourceDiff rd = resDiff.get(name); + // Filter all resources with a resource diff of kind MODIFIED. + // Note that REMOVED won't happen since in that case the module listing + // won't have the resource anyway. + // Note as well that filter removes files of kind ADDED. Those files are + // not in the packaged modules, so ought not to get returned from the + // pipeline. + return (rd == null || rd.getKind() == ResourceDiff.Kind.MODIFIED); + } + private void collectFiles() throws IOException { if (files.isEmpty()) { addNonClassResources(); + // Add classes/resources from the run-time image, // patched with the run-time image diff - files.addAll(ref.open().list() - .filter(i -> { - String lookupKey = String.format("/%s/%s", module, i); - ResourceDiff rd = resDiff.get(lookupKey); - // Filter all resources with a resource diff - // that are of kind MODIFIED. - // Note that REMOVED won't happen since in - // that case the module listing won't have - // the resource anyway. - // Note as well that filter removes files - // of kind ADDED. Those files are not in - // the packaged modules, so ought not to - // get returned from the pipeline. - return (rd == null || - rd.getKind() == ResourceDiff.Kind.MODIFIED); - }) - .map(s -> { - String lookupKey = String.format("/%s/%s", module, s); - return new JRTArchiveFile(JRTArchive.this, s, - EntryType.CLASS_OR_RESOURCE, - null /* hashOrTarget */, - false /* symlink */, - resDiff.get(lookupKey)); - }) - .toList()); + imageResources.getEntryNames(module) + .filter(this::isNormalOrModifiedDiff) + .sorted() + .map(name -> new JrtModuleFile(this, name, resDiff.get(name))) + .forEach(files::add); + // Finally add all files only present in the resource diff // That is, removed items in the run-time image. files.addAll(resDiff.values().stream() - .filter(rd -> rd.getKind() == ResourceDiff.Kind.REMOVED) - .map(s -> { - int secondSlash = s.getName().indexOf("/", 1); - assert secondSlash != -1; - String pathWithoutModule = s.getName().substring(secondSlash + 1); - return new JRTArchiveFile(JRTArchive.this, - pathWithoutModule, - EntryType.CLASS_OR_RESOURCE, - null /* hashOrTarget */, - false /* symlink */, - s); - }) - .toList()); + .filter(rd -> rd.getKind() == ResourceDiff.Kind.REMOVED) + .map(rd -> new JrtModuleFile(this, rd.getName(), rd)) + .toList()); } } @@ -234,15 +217,10 @@ private void addNonClassResources() { } } - return new JRTArchiveFile(JRTArchive.this, - m.resPath, - toEntryType(m.resType), - m.hashOrTarget, - m.symlink, - /* diff only for resources */ - null); - }) - .toList()); + return new JrtOtherFile( + this, m.resPath, toEntryType(m.resType), m.hashOrTarget, m.symlink); + }) + .toList()); } } @@ -323,11 +301,11 @@ public String encodeToString() { resPath); } - /** + /* * line: ||| * - * Take the integer before '|' convert it to a Type. The second - * token is an integer representing symlinks (or not). The third token is + * Take the integer before '|' convert it to a Type. The second token + * is an integer representing symlinks (or not). The third token is * a hash sum (sha512) of the file denoted by the fourth token (path). */ static ResourceFileEntry decodeFromString(String line) { @@ -437,47 +415,75 @@ interface JRTFile { Entry toEntry(); } - record JRTArchiveFile(Archive archive, - String resPath, - EntryType resType, - String sha, - boolean symlink, - ResourceDiff diff) implements JRTFile { + record JrtModuleFile( + JRTArchive archive, + String resPath, + ResourceDiff diff) implements JRTFile { + @Override + public Entry toEntry() { + assert resPath.startsWith("/" + archive.moduleName() + "/"); + String resName = resPath.substring(archive.moduleName().length() + 2); + + // If the resource has a diff to the packaged modules, use the diff. + // Diffs of kind ADDED have been filtered out in collectFiles(); + if (diff != null) { + assert diff.getKind() != ResourceDiff.Kind.ADDED; + assert diff.getName().equals(resPath); + + return new Entry(archive, resPath, resName, EntryType.CLASS_OR_RESOURCE) { + @Override + public long size() { + return diff.getResourceBytes().length; + } + @Override + public InputStream stream() { + return new ByteArrayInputStream(diff.getResourceBytes()); + } + }; + } else { + return new Entry(archive, resPath, resName, EntryType.CLASS_OR_RESOURCE) { + @Override + public long size() { + return archive.imageResources.getSize(resPath); + } + + @Override + public InputStream stream() { + // Byte content could be cached in the entry if needed. + return new ByteArrayInputStream(archive.imageResources.getBytes(resPath)); + } + }; + } + } + } + + record JrtOtherFile( + JRTArchive archive, + String resPath, + EntryType resType, + String sha, + boolean symlink) implements JRTFile { + + // Read from the base JDK image, special casing + // symlinks, which have the link target in the + // hashOrTarget field. + Path targetPath() { + return BASE.resolve(symlink ? sha : resPath); + } + public Entry toEntry() { - return new Entry(archive, - String.format("/%s/%s", - archive.moduleName(), - resPath), - resPath, - resType) { + assert resType != EntryType.CLASS_OR_RESOURCE; + + return new Entry( + archive, + String.format("/%s/%s", archive.moduleName(), resPath), + resPath, + resType) { + @Override public long size() { try { - if (resType != EntryType.CLASS_OR_RESOURCE) { - // Read from the base JDK image, special casing - // symlinks, which have the link target in the - // hashOrTarget field - if (symlink) { - return Files.size(BASE.resolve(sha)); - } - return Files.size(BASE.resolve(resPath)); - } else { - if (diff != null) { - // If the resource has a diff to the - // packaged modules, use the diff. Diffs of kind - // ADDED have been filtered out in collectFiles(); - assert diff.getKind() != ResourceDiff.Kind.ADDED; - assert diff.getName().equals(String.format("/%s/%s", - archive.moduleName(), - resPath)); - return diff.getResourceBytes().length; - } - // Read from the module image. This works, because - // the underlying base path is a JrtPath with the - // JrtFileSystem underneath which is able to handle - // this size query. - return Files.size(archive.getPath().resolve(resPath)); - } + return Files.size(targetPath()); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -485,28 +491,8 @@ public long size() { @Override public InputStream stream() throws IOException { - if (resType != EntryType.CLASS_OR_RESOURCE) { - // Read from the base JDK image. - Path path = symlink ? BASE.resolve(sha) : BASE.resolve(resPath); - return Files.newInputStream(path); - } else { - // Read from the module image. Use the diff to the - // packaged modules if we have one. Diffs of kind - // ADDED have been filtered out in collectFiles(); - if (diff != null) { - assert diff.getKind() != ResourceDiff.Kind.ADDED; - assert diff.getName().equals(String.format("/%s/%s", - archive.moduleName(), - resPath)); - return new ByteArrayInputStream(diff.getResourceBytes()); - } - String module = archive.moduleName(); - ModuleReference mRef = ModuleFinder.ofSystem() - .find(module).orElseThrow(); - return mRef.open().open(resPath).orElseThrow(); - } + return Files.newInputStream(targetPath()); } - }; } } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java index 928b9a47934..0fa4cb4b142 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -349,21 +349,22 @@ public static void createImage(JlinkConfiguration config, plugins = plugins == null ? new PluginsConfiguration() : plugins; // First create the image provider - ImageProvider imageProvider = - createImageProvider(config, - null, - IGNORE_SIGNING_DEFAULT, - false, - null, - false, - new OptionsValues(), - null); - - // Then create the Plugin Stack - ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(plugins); - - //Ask the stack to proceed; - stack.operate(imageProvider); + try (ImageHelper imageProvider = + createImageProvider(config, + null, + IGNORE_SIGNING_DEFAULT, + false, + null, + false, + new OptionsValues(), + null)) { + + // Then create the Plugin Stack + ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(plugins); + + // Ask the stack to proceed; + stack.operate(imageProvider); + } } // the token for "all modules on the module path" @@ -486,22 +487,24 @@ private void createImage(JlinkConfiguration config) throws Exception { } // First create the image provider - ImageHelper imageProvider = createImageProvider(config, - options.packagedModulesPath, - options.ignoreSigning, - options.bindServices, - options.endian, - options.verbose, - options, - log); - - // Then create the Plugin Stack - ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration( - taskHelper.getPluginsConfig(options.output, options.launchers, - imageProvider.targetPlatform)); - - //Ask the stack to proceed - stack.operate(imageProvider); + try (ImageHelper imageProvider = createImageProvider(config, + options.packagedModulesPath, + options.ignoreSigning, + options.bindServices, + options.endian, + options.verbose, + options, + log)) { + // Then create the Plugin Stack + ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration( + taskHelper.getPluginsConfig( + options.output, + options.launchers, + imageProvider.targetPlatform)); + + //Ask the stack to proceed + stack.operate(imageProvider); + } } /** @@ -1027,10 +1030,11 @@ private String getSaveOpts() { return sb.toString(); } - private static record ImageHelper(Set archives, - Platform targetPlatform, - Path packagedModulesPath, - boolean generateRuntimeImage) implements ImageProvider { + private record ImageHelper(Set archives, + Platform targetPlatform, + Path packagedModulesPath, + boolean generateRuntimeImage) + implements ImageProvider, AutoCloseable { @Override public ExecutableImage retrieve(ImagePluginStack stack) throws IOException { ExecutableImage image = ImageFileCreator.create(archives, @@ -1046,5 +1050,25 @@ public ExecutableImage retrieve(ImagePluginStack stack) throws IOException { } return image; } + + @Override + public void close() throws IOException { + List thrown = null; + for (Archive archive : archives) { + try { + archive.close(); + } catch (IOException ex) { + if (thrown == null) { + thrown = new ArrayList<>(); + } + thrown.add(ex); + } + } + if (thrown != null) { + IOException ex = new IOException("Archives could not be closed", thrown.getFirst()); + thrown.subList(1, thrown.size()).forEach(ex::addSuppressed); + throw ex; + } + } } } diff --git a/test/jdk/tools/jlink/runtimeImage/PackagedModulesVsRuntimeImageLinkTest.java b/test/jdk/tools/jlink/runtimeImage/PackagedModulesVsRuntimeImageLinkTest.java index ef7030d2e62..3ce58fe5684 100644 --- a/test/jdk/tools/jlink/runtimeImage/PackagedModulesVsRuntimeImageLinkTest.java +++ b/test/jdk/tools/jlink/runtimeImage/PackagedModulesVsRuntimeImageLinkTest.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2024, Red Hat, Inc. + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -31,6 +32,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; import tests.Helper; import tests.JImageGenerator; @@ -47,9 +52,10 @@ * jdk.jlink/jdk.tools.jlink.internal * jdk.jlink/jdk.tools.jlink.plugin * jdk.jlink/jdk.tools.jimage + * jdk.jlink * @build tests.* jdk.test.lib.process.OutputAnalyzer * jdk.test.lib.process.ProcessTools - * @run main/othervm/timeout=1200 -Xmx1g PackagedModulesVsRuntimeImageLinkTest + * @run main/othervm/timeout=1200 -ea -esa -DDISABLE_PREVIEW_PATCHING=false -Xmx1g PackagedModulesVsRuntimeImageLinkTest */ public class PackagedModulesVsRuntimeImageLinkTest extends AbstractLinkableRuntimeTest { @@ -70,7 +76,6 @@ void runTest(Helper helper, boolean isLinkableRuntime) throws Exception { builder.setLinkableRuntime(); } Path javaSEruntimeLink = createJavaImageRuntimeLink(builder.build()); - // create a java.se using packaged modules (jmod-full) Path javaSEJmodFull = JImageGenerator.getJLinkTask() .output(helper.createNewImageDir("java-se-jmodfull")) @@ -120,9 +125,8 @@ private static void compareRecursively(Path javaSEJmodLess, Path jimageJmodFull = javaSEJmodFull.resolve(Path.of("lib")).resolve(Path.of("modules")); List jimageContentJmodLess = JImageHelper.listContents(jimageJmodLess); List jimageContentJmodFull = JImageHelper.listContents(jimageJmodFull); - if (jimageContentJmodLess.size() != jimageContentJmodFull.size()) { - throw new AssertionError(String.format("Size of jimage content differs for jmod-less (%d) v. jmod-full (%d)", jimageContentJmodLess.size(), jimageContentJmodFull.size())); - } + assertSameContent("jmod-less", jimageContentJmodLess, "jmod-full", jimageContentJmodFull); + // Both lists are same size, with same names, so enumerate either. for (int i = 0; i < jimageContentJmodFull.size(); i++) { if (!jimageContentJmodFull.get(i).equals(jimageContentJmodLess.get(i))) { throw new AssertionError(String.format("Jimage content differs at index %d: jmod-full was: '%s' jmod-less was: '%s'", @@ -145,8 +149,37 @@ private static void compareRecursively(Path javaSEJmodLess, } } + // Helper to assert the content of two jimage files are the same and provide + // useful debug information otherwise. + private static void assertSameContent( + String lhsLabel, List lhsNames, String rhsLabel, List rhsNames) { + + List lhsOnly = + lhsNames.stream().filter(Predicate.not(Set.copyOf(rhsNames)::contains)).toList(); + List rhsOnly = + rhsNames.stream().filter(Predicate.not(Set.copyOf(lhsNames)::contains)).toList(); + if (!lhsOnly.isEmpty() || !rhsOnly.isEmpty()) { + String message = String.format( + "jimage content differs for %s (%d) v. %s (%d)", + lhsLabel, lhsNames.size(), rhsLabel, rhsNames.size()); + if (!lhsOnly.isEmpty()) { + message += "\nOnly in " + lhsLabel + ":\n\t" + String.join("\n\t", lhsOnly); + } + if (!rhsOnly.isEmpty()) { + message += "\nOnly in " + rhsLabel + ":\n\t" + String.join("\n\t", rhsOnly); + } + throw new AssertionError(message); + } + } + private static boolean isTreeInfoResource(String path) { - return path.startsWith("/packages") || path.startsWith("/modules"); + return pathStartsWith(path, "/packages") || pathStartsWith(path, "/modules"); + } + + // Handle both "" and "/...". + private static boolean pathStartsWith(String path, String prefix) { + int plen = prefix.length(); + return path.startsWith(prefix) && (path.length() == plen || path.charAt(plen) == '/'); } private static void handleFileMismatch(Path a, Path b) {