diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageHeader.java b/src/java.base/share/classes/jdk/internal/jimage/ImageHeader.java index f63665119e2..ab5b7f0111e 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageHeader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageHeader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 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 @@ -30,6 +30,23 @@ import java.util.Objects; /** + * Defines the header and version information for jimage files. + * + *

Version number changes must be synced in a single change across all code + * which reads/writes jimage files, and code which tries to open a jimage file + * with an unexpected version should fail. + * + *

Known jimage file code which needs updating on version change: + *

+ * + *

Version history: + *

+ * * @implNote This class needs to maintain JDK 8 source compatibility. * * It is used internally in the JDK to implement jimage/jrtfs access, @@ -39,7 +56,7 @@ public final class ImageHeader { public static final int MAGIC = 0xCAFEDADA; public static final int MAJOR_VERSION = 1; - public static final int MINOR_VERSION = 0; + public static final int MINOR_VERSION = 1; private static final int HEADER_SLOTS = 7; private final int magic; diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageLocation.java b/src/java.base/share/classes/jdk/internal/jimage/ImageLocation.java index f31c7291927..952bbe3cbde 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageLocation.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageLocation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 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 @@ -27,6 +27,7 @@ import java.nio.ByteBuffer; import java.util.Objects; +import java.util.function.Predicate; /** * @implNote This class needs to maintain JDK 8 source compatibility. @@ -44,7 +45,102 @@ public class ImageLocation { public static final int ATTRIBUTE_OFFSET = 5; public static final int ATTRIBUTE_COMPRESSED = 6; public static final int ATTRIBUTE_UNCOMPRESSED = 7; - public static final int ATTRIBUTE_COUNT = 8; + public static final int ATTRIBUTE_PREVIEW_FLAGS = 8; + public static final int ATTRIBUTE_COUNT = 9; + + // Flag masks for the ATTRIBUTE_PREVIEW_FLAGS attribute. Defined so + // that zero is the overwhelmingly common case for normal resources. + + /** + * Indicates that a non-preview location is associated with preview + * resources. + * + *

This can apply to both resources and directories in the + * {@code /modules/xxx/...} namespace, as well as {@code /packages/xxx} + * directories. + * + *

For {@code /packages/xxx} directories, it indicates that the package + * has preview resources in one of the modules in which it exists. + */ + public static final int FLAGS_HAS_PREVIEW_VERSION = 0x1; + /** + * Set on all locations in the {@code /modules/xxx/META-INF/preview/...} + * namespace. + * + *

This flag is mutually exclusive with {@link #FLAGS_HAS_PREVIEW_VERSION}. + */ + public static final int FLAGS_IS_PREVIEW_VERSION = 0x2; + /** + * Indicates that a location only exists due to preview resources. + * + *

This can apply to both resources and directories in the + * {@code /modules/xxx/...} namespace, as well as {@code /packages/xxx} + * directories. + * + *

For {@code /packages/xxx} directories it indicates that, for every + * module in which the package exists, it is preview only. + * + *

This flag is mutually exclusive with {@link #FLAGS_HAS_PREVIEW_VERSION} + * and need not imply that {@link #FLAGS_IS_PREVIEW_VERSION} is set (i.e. + * for {@code /packages/xxx} directories). + */ + public static final int FLAGS_IS_PREVIEW_ONLY = 0x4; + /** + * This flag identifies the unique {@code "/packages"} location, and + * is used to determine the {@link LocationType} without additional + * string comparison. + * + *

This flag is mutually exclusive with all other flags. + */ + public static final int FLAGS_IS_PACKAGE_ROOT = 0x8; + + // Also used in ImageReader. + static final String MODULES_PREFIX = "/modules"; + static final String PACKAGES_PREFIX = "/packages"; + static final String PREVIEW_INFIX = "/META-INF/preview"; + + /** + * Helper function to calculate preview flags (ATTRIBUTE_PREVIEW_FLAGS). + * + *

Since preview flags are calculated separately for resource nodes and + * directory nodes (in two quite different places) it's useful to have a + * common helper. + * + * @param name the jimage name of the resource or directory. + * @param hasEntry a predicate for jimage names returning whether an entry + * is present. + * @return flags for the ATTRIBUTE_PREVIEW_FLAGS attribute. + */ + public static int getFlags(String name, Predicate hasEntry) { + if (name.startsWith(PACKAGES_PREFIX + "/")) { + throw new IllegalArgumentException("Package sub-directory flags handled separately: " + name); + } + String start = name.startsWith(MODULES_PREFIX + "/") ? MODULES_PREFIX + "/" : "/"; + int idx = name.indexOf('/', start.length()); + if (idx == -1) { + // Special case for "/packages" root, but otherwise, no flags. + return name.equals(PACKAGES_PREFIX) ? FLAGS_IS_PACKAGE_ROOT : 0; + } + String prefix = name.substring(0, idx); + String suffix = name.substring(idx); + if (suffix.startsWith(PREVIEW_INFIX + "/")) { + // Preview resources/directories. + String nonPreviewName = prefix + suffix.substring(PREVIEW_INFIX.length()); + return FLAGS_IS_PREVIEW_VERSION + | (hasEntry.test(nonPreviewName) ? 0 : FLAGS_IS_PREVIEW_ONLY); + } else if (!suffix.startsWith("/META-INF/")) { + // Non-preview resources/directories. + String previewName = prefix + PREVIEW_INFIX + suffix; + return hasEntry.test(previewName) ? FLAGS_HAS_PREVIEW_VERSION : 0; + } else { + // Edge case for things META-INF/module-info.class etc. + return 0; + } + } + + public enum LocationType { + RESOURCE, MODULES_ROOT, MODULES_DIR, PACKAGES_ROOT, PACKAGES_DIR; + } protected final long[] attributes; @@ -285,6 +381,10 @@ public int getExtensionOffset() { return (int)getAttribute(ATTRIBUTE_EXTENSION); } + public int getFlags() { + return (int) getAttribute(ATTRIBUTE_PREVIEW_FLAGS); + } + public String getFullName() { return getFullName(false); } @@ -294,7 +394,7 @@ public String getFullName(boolean modulesPrefix) { if (getModuleOffset() != 0) { if (modulesPrefix) { - builder.append("/modules"); + builder.append(MODULES_PREFIX); } builder.append('/'); @@ -317,36 +417,6 @@ public String getFullName(boolean modulesPrefix) { return builder.toString(); } - String buildName(boolean includeModule, boolean includeParent, - boolean includeName) { - StringBuilder builder = new StringBuilder(); - - if (includeModule && getModuleOffset() != 0) { - builder.append("/modules/"); - builder.append(getModule()); - } - - if (includeParent && getParentOffset() != 0) { - builder.append('/'); - builder.append(getParent()); - } - - if (includeName) { - if (includeModule || includeParent) { - builder.append('/'); - } - - builder.append(getBase()); - - if (getExtensionOffset() != 0) { - builder.append('.'); - builder.append(getExtension()); - } - } - - return builder.toString(); - } - public long getContentOffset() { return getAttribute(ATTRIBUTE_OFFSET); } @@ -359,6 +429,48 @@ public long getUncompressedSize() { return getAttribute(ATTRIBUTE_UNCOMPRESSED); } + // Fast (zero allocation) type determination for locations. + public LocationType getType() { + switch (getModuleOffset()) { + case ImageStrings.MODULES_STRING_OFFSET: + // Locations in /modules/... namespace are directory entries. + return LocationType.MODULES_DIR; + case ImageStrings.PACKAGES_STRING_OFFSET: + // Locations in /packages/... namespace are always 2-level + // "/packages/xxx" directories. + return LocationType.PACKAGES_DIR; + case ImageStrings.EMPTY_STRING_OFFSET: + // Only 2 choices, either the "/modules" or "/packages" root. + assert isRootDir() : "Invalid root directory: " + getFullName(); + + // Temporary logic to handle package root classification until new + // image reader code is committed which sets FLAGS_IS_PACKAGE_ROOT. + // Base name is "/packages" or "/modules" (NOT "packages" and "modules"). + // TODO: Uncomment the FLAGS_IS_PACKAGE_ROOT test below. + // return (getFlags() & FLAGS_IS_PACKAGE_ROOT) != 0 + return getBase().charAt(1) == 'p' + ? LocationType.PACKAGES_ROOT + : LocationType.MODULES_ROOT; + default: + // Anything else is // and references a resource. + return LocationType.RESOURCE; + } + } + + private boolean isRootDir() { + if (getModuleOffset() == 0 && getParentOffset() == 0) { + String name = getFullName(); + return name.equals(MODULES_PREFIX) || name.equals(PACKAGES_PREFIX); + } + return false; + } + + @Override + public String toString() { + // Cannot use String.format() (too early in startup for locale code). + return "ImageLocation[name='" + getFullName() + "', type=" + getType() + ", flags=" + getFlags() + "]"; + } + static ImageLocation readFrom(BasicImageReader reader, int offset) { Objects.requireNonNull(reader); long[] attributes = reader.getAttributes(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 e062e1629ff..00811da2f05 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java @@ -24,6 +24,8 @@ */ package jdk.internal.jimage; +import jdk.internal.jimage.ImageLocation.LocationType; + import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -34,16 +36,28 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.TreeMap; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; +import static jdk.internal.jimage.ImageLocation.FLAGS_HAS_PREVIEW_VERSION; +import static jdk.internal.jimage.ImageLocation.FLAGS_IS_PREVIEW_ONLY; +import static jdk.internal.jimage.ImageLocation.LocationType.MODULES_DIR; +import static jdk.internal.jimage.ImageLocation.LocationType.MODULES_ROOT; +import static jdk.internal.jimage.ImageLocation.LocationType.PACKAGES_DIR; +import static jdk.internal.jimage.ImageLocation.LocationType.RESOURCE; +import static jdk.internal.jimage.ImageLocation.MODULES_PREFIX; +import static jdk.internal.jimage.ImageLocation.PACKAGES_PREFIX; +import static jdk.internal.jimage.ImageLocation.PREVIEW_INFIX; + /** * A view over the entries of a jimage file with a unified namespace suitable * for file system use. The jimage entries (resources, module and package @@ -86,22 +100,26 @@ private ImageReader(SharedImageReader reader) { } /** - * Opens an image reader for a jimage file at the specified path, using the - * given byte order. + * Opens an image reader for a jimage file at the specified path. + * + * @param imagePath file system path of the jimage file. + * @param mode whether to return preview resources. */ - public static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException { - Objects.requireNonNull(imagePath); - Objects.requireNonNull(byteOrder); - - return SharedImageReader.open(imagePath, byteOrder); + public static ImageReader open(Path imagePath, PreviewMode mode) throws IOException { + return open(imagePath, ByteOrder.nativeOrder(), mode); } /** - * Opens an image reader for a jimage file at the specified path, using the - * platform native byte order. + * Opens an image reader for a jimage file at the specified path. + * + * @param imagePath file system path of the jimage file. + * @param byteOrder the byte-order to be used when reading the jimage file. + * @param mode controls whether preview resources are visible. */ - public static ImageReader open(Path imagePath) throws IOException { - return open(imagePath, ByteOrder.nativeOrder()); + public static ImageReader open(Path imagePath, ByteOrder byteOrder, PreviewMode mode) throws IOException { + Objects.requireNonNull(imagePath); + Objects.requireNonNull(byteOrder); + return SharedImageReader.open(imagePath, byteOrder, mode.resolve()); } @Override @@ -214,14 +232,38 @@ public ByteBuffer getResourceBuffer(Node node) { } private static final class SharedImageReader extends BasicImageReader { - private static final Map OPEN_FILES = new HashMap<>(); - private static final String MODULES_ROOT = "/modules"; - private static final String PACKAGES_ROOT = "/packages"; // 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 // 1000 classes. Thus, an initial capacity of 2000 is a reasonable guess. private static final int INITIAL_NODE_CACHE_CAPACITY = 2000; + static final class ReaderKey { + private final Path imagePath; + private final boolean previewMode; + + public ReaderKey(Path imagePath, boolean previewMode) { + this.imagePath = imagePath; + this.previewMode = previewMode; + } + + @Override + public boolean equals(Object obj) { + // No pattern variables here (Java 8 compatible source). + if (obj instanceof ReaderKey) { + ReaderKey other = (ReaderKey) obj; + return this.imagePath.equals(other.imagePath) && this.previewMode == other.previewMode; + } + return false; + } + + @Override + public int hashCode() { + return imagePath.hashCode() ^ Boolean.hashCode(previewMode); + } + } + + private static final Map OPEN_FILES = new HashMap<>(); + // List of openers for this shared image. private final Set openers = new HashSet<>(); @@ -232,55 +274,145 @@ private static final class SharedImageReader extends BasicImageReader { // Cache of all user visible nodes, guarded by synchronizing 'this' instance. private final Map nodes; - // Used to classify ImageLocation instances without string comparison. - private final int modulesStringOffset; - private final int packagesStringOffset; - private SharedImageReader(Path imagePath, ByteOrder byteOrder) throws IOException { + // Preview mode support. + private final boolean previewMode; + // A relativized mapping from non-preview name to directories containing + // preview-only nodes. This is used to add preview-only content to + // directories as they are completed. + private final HashMap previewDirectoriesToMerge; + + private SharedImageReader(Path imagePath, ByteOrder byteOrder, boolean previewMode) throws IOException { super(imagePath, byteOrder); this.imageFileAttributes = Files.readAttributes(imagePath, BasicFileAttributes.class); this.nodes = new HashMap<>(INITIAL_NODE_CACHE_CAPACITY); - // Pick stable jimage names from which to extract string offsets (we cannot - // use "/modules" or "/packages", since those have a module offset of zero). - this.modulesStringOffset = getModuleOffset("/modules/java.base"); - this.packagesStringOffset = getModuleOffset("/packages/java.lang"); + this.previewMode = previewMode; // Node creation is very lazy, so we can just make the top-level directories // now without the risk of triggering the building of lots of other nodes. - Directory packages = newDirectory(PACKAGES_ROOT); - nodes.put(packages.getName(), packages); - Directory modules = newDirectory(MODULES_ROOT); - nodes.put(modules.getName(), modules); + Directory packages = ensureCached(newDirectory(PACKAGES_PREFIX)); + Directory modules = ensureCached(newDirectory(MODULES_PREFIX)); Directory root = newDirectory("/"); root.setChildren(Arrays.asList(packages, modules)); - nodes.put(root.getName(), root); + ensureCached(root); + + // By scanning the /packages directory information early we can determine + // which module/package pairs have preview resources, and build the (small) + // set of preview nodes early. This also ensures that preview-only entries + // in the /packages directory are not present in non-preview mode. + this.previewDirectoriesToMerge = previewMode ? new HashMap<>() : null; + packages.setChildren(processPackagesDirectory(previewMode)); } /** - * Returns the offset of the string denoting the leading "module" segment in - * the given path (e.g. {@code /}). We can't just pass in the - * {@code /} string here because that has a module offset of zero. + * Process {@code "/packages/xxx"} entries to build the child nodes for the + * root {@code "/packages"} node. Preview-only entries will be skipped if + * {@code previewMode == false}. + * + *

If {@code previewMode == true}, this method also populates the {@link + * #previewDirectoriesToMerge} map with any preview-only nodes, to be merged + * into directories as they are completed. It also caches preview resources + * and preview-only directories for direct lookup. */ - private int getModuleOffset(String path) { - ImageLocation location = findLocation(path); - assert location != null : "Cannot find expected jimage location: " + path; - int offset = location.getModuleOffset(); - assert offset != 0 : "Invalid module offset for jimage location: " + path; - return offset; + private ArrayList processPackagesDirectory(boolean previewMode) { + ImageLocation pkgRoot = findLocation(PACKAGES_PREFIX); + assert pkgRoot != null : "Invalid jimage file"; + IntBuffer offsets = getOffsetBuffer(pkgRoot); + ArrayList pkgDirs = new ArrayList<>(offsets.capacity()); + // Package path to module map, sorted in reverse order so that + // longer child paths get processed first. + Map> previewPackagesToModules = + new TreeMap<>(Comparator.reverseOrder()); + for (int i = 0; i < offsets.capacity(); i++) { + ImageLocation pkgDir = getLocation(offsets.get(i)); + int flags = pkgDir.getFlags(); + // A package subdirectory is "preview only" if all the modules + // it references have that package marked as preview only. + // Skipping these entries avoids empty package subdirectories. + if (previewMode || (flags & FLAGS_IS_PREVIEW_ONLY) == 0) { + pkgDirs.add(ensureCached(newDirectory(pkgDir.getFullName()))); + } + if (!previewMode || (flags & FLAGS_HAS_PREVIEW_VERSION) == 0) { + continue; + } + // Only do this in preview mode for the small set of packages with + // preview versions (the number of preview entries should be small). + List moduleNames = new ArrayList<>(); + ModuleReference.readNameOffsets(getOffsetBuffer(pkgDir), /*normal*/ false, /*preview*/ true) + .forEachRemaining(n -> moduleNames.add(getString(n))); + previewPackagesToModules.put(pkgDir.getBase().replace('.', '/'), moduleNames); + } + // Reverse sorted map means child directories are processed first. + previewPackagesToModules.forEach((pkgPath, modules) -> + modules.forEach(modName -> processPreviewDir(MODULES_PREFIX + "/" + modName, pkgPath))); + // We might have skipped some preview-only package entries. + pkgDirs.trimToSize(); + return pkgDirs; + } + + void processPreviewDir(String namePrefix, String pkgPath) { + String previewDirName = namePrefix + PREVIEW_INFIX + "/" + pkgPath; + ImageLocation previewLoc = findLocation(previewDirName); + assert previewLoc != null : "Missing preview directory location: " + previewDirName; + String nonPreviewDirName = namePrefix + "/" + pkgPath; + List previewOnlyChildren = createChildNodes(previewLoc, 0, childLoc -> { + String baseName = getBaseName(childLoc); + String nonPreviewChildName = nonPreviewDirName + "/" + baseName; + boolean isPreviewOnly = (childLoc.getFlags() & FLAGS_IS_PREVIEW_ONLY) != 0; + LocationType type = childLoc.getType(); + if (type == RESOURCE) { + // Preview resources are cached to override non-preview versions. + Node childNode = ensureCached(newResource(nonPreviewChildName, childLoc)); + return isPreviewOnly ? childNode : null; + } else { + // Child directories are not cached here (they are either cached + // already or have been added to previewDirectoriesToMerge). + assert type == MODULES_DIR : "Invalid location type: " + childLoc; + Node childNode = nodes.get(nonPreviewChildName); + assert isPreviewOnly == (childNode != null) : + "Inconsistent child node: " + nonPreviewChildName; + return childNode; + } + }); + Directory previewDir = newDirectory(nonPreviewDirName); + previewDir.setChildren(previewOnlyChildren); + if ((previewLoc.getFlags() & FLAGS_IS_PREVIEW_ONLY) != 0) { + // If we are preview-only, our children are also preview-only, so + // this directory is a complete hierarchy and should be cached. + assert !previewOnlyChildren.isEmpty() : "Invalid empty preview-only directory: " + nonPreviewDirName; + ensureCached(previewDir); + } else if (!previewOnlyChildren.isEmpty()) { + // A partial directory containing extra preview-only nodes + // to be merged when the non-preview directory is completed. + previewDirectoriesToMerge.put(nonPreviewDirName, previewDir); + } } - private static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException { + // Adds a node to the cache, ensuring that no matching entry already existed. + private T ensureCached(T node) { + Node existingNode = nodes.put(node.getName(), node); + assert existingNode == null : "Unexpected node already cached for: " + node; + return node; + } + + // As above but ignores null. + private T ensureCachedIfNonNull(T node) { + return node != null ? ensureCached(node) : null; + } + + private static ImageReader open(Path imagePath, ByteOrder byteOrder, boolean previewMode) throws IOException { Objects.requireNonNull(imagePath); Objects.requireNonNull(byteOrder); synchronized (OPEN_FILES) { - SharedImageReader reader = OPEN_FILES.get(imagePath); + ReaderKey key = new ReaderKey(imagePath, previewMode); + SharedImageReader reader = OPEN_FILES.get(key); if (reader == null) { // Will fail with an IOException if wrong byteOrder. - reader = new SharedImageReader(imagePath, byteOrder); - OPEN_FILES.put(imagePath, reader); + reader = new SharedImageReader(imagePath, byteOrder, previewMode); + OPEN_FILES.put(key, reader); } else if (reader.getByteOrder() != byteOrder) { throw new IOException("\"" + reader.getName() + "\" is not an image file"); } @@ -304,7 +436,7 @@ public void close(ImageReader image) throws IOException { close(); nodes.clear(); - if (!OPEN_FILES.remove(this.getImagePath(), this)) { + if (!OPEN_FILES.remove(new ReaderKey(getImagePath(), previewMode), this)) { throw new IOException("image file not found in open list"); } } @@ -322,20 +454,14 @@ public void close(ImageReader image) throws IOException { * "/modules" or "/packages". */ synchronized Node findNode(String name) { + // Root directories "/", "/modules" and "/packages", as well + // as all "/packages/xxx" subdirectories are already cached. Node node = nodes.get(name); if (node == null) { - // We cannot get the root paths ("/modules" or "/packages") here - // because those nodes are already in the nodes cache. - if (name.startsWith(MODULES_ROOT + "/")) { - // This may perform two lookups, one for a directory (in - // "/modules/...") and one for a non-prefixed resource - // (with "/modules" removed). - node = buildModulesNode(name); - } else if (name.startsWith(PACKAGES_ROOT + "/")) { - node = buildPackagesNode(name); - } - if (node != null) { - nodes.put(node.getName(), node); + if (name.startsWith(MODULES_PREFIX + "/")) { + node = buildAndCacheModulesNode(name); + } else if (name.startsWith(PACKAGES_PREFIX + "/")) { + node = buildAndCacheLinkNode(name); } } else if (!node.isCompleted()) { // Only directories can be incomplete. @@ -359,13 +485,13 @@ Node findResourceNode(String moduleName, String resourcePath) { if (moduleName.indexOf('/') >= 0) { throw new IllegalArgumentException("invalid module name: " + moduleName); } - String nodeName = MODULES_ROOT + "/" + moduleName + "/" + resourcePath; + String nodeName = MODULES_PREFIX + "/" + moduleName + "/" + resourcePath; // Synchronize as tightly as possible to reduce locking contention. synchronized (this) { Node node = nodes.get(nodeName); if (node == null) { ImageLocation loc = findLocation(moduleName, resourcePath); - if (loc != null && isResource(loc)) { + if (loc != null && loc.getType() == RESOURCE) { node = newResource(nodeName, loc); nodes.put(node.getName(), node); } @@ -381,18 +507,29 @@ Node findResourceNode(String moduleName, String resourcePath) { * *

This method is expected to be called frequently for resources * which do not exist in the given module (e.g. as part of classpath - * search). As such, it skips checking the nodes cache and only checks - * for an entry in the jimage file, as this is faster if the resource - * is not present. This also means it doesn't need synchronization. + * search). As such, it skips checking the nodes cache if possible, and + * only checks for an entry in the jimage file, as this is faster if the + * resource is not present. This also means it doesn't need + * synchronization most of the time. */ boolean containsResource(String moduleName, String resourcePath) { if (moduleName.indexOf('/') >= 0) { throw new IllegalArgumentException("invalid module name: " + moduleName); } - // If the given module name is 'modules', then 'isResource()' - // returns false to prevent false positives. + // In preview mode, preview-only resources are eagerly added to the + // cache, so we must check that first. + if (previewMode) { + String nodeName = MODULES_PREFIX + "/" + moduleName + "/" + resourcePath; + // Synchronize as tightly as possible to reduce locking contention. + synchronized (this) { + Node node = nodes.get(nodeName); + if (node != null) { + return node.isResource(); + } + } + } ImageLocation loc = findLocation(moduleName, resourcePath); - return loc != null && isResource(loc); + return loc != null && loc.getType() == RESOURCE; } /** @@ -401,55 +538,81 @@ boolean containsResource(String moduleName, String resourcePath) { *

Called by {@link #findNode(String)} if a {@code /modules/...} node * is not present in the cache. */ - private Node buildModulesNode(String name) { - assert name.startsWith(MODULES_ROOT + "/") : "Invalid module node name: " + name; + private Node buildAndCacheModulesNode(String name) { + assert name.startsWith(MODULES_PREFIX + "/") : "Invalid module node name: " + name; + if (isPreviewName(name)) { + return null; + } // Returns null for non-directory resources, since the jimage name does not // start with "/modules" (e.g. "/java.base/java/lang/Object.class"). ImageLocation loc = findLocation(name); if (loc != null) { assert name.equals(loc.getFullName()) : "Mismatched location for directory: " + name; - assert isModulesSubdirectory(loc) : "Invalid modules directory: " + name; - return completeModuleDirectory(newDirectory(name), loc); + assert loc.getType() == MODULES_DIR : "Invalid modules directory: " + name; + return ensureCached(completeModuleDirectory(newDirectory(name), loc)); } // 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_ROOT.length())); - return loc != null && isResource(loc) ? newResource(name, loc) : null; + loc = findLocation(name.substring(MODULES_PREFIX.length())); + return ensureCachedIfNonNull( + loc != null && loc.getType() == RESOURCE ? newResource(name, loc) : null); } /** - * Builds a node in the "/packages/..." namespace. + * Returns whether a directory name in the "/modules/" directory could be referencing + * the "META-INF" directory". + */ + private boolean isMetaInf(Directory dir) { + String name = dir.getName(); + int pathStart = name.indexOf('/', MODULES_PREFIX.length() + 1); + return name.length() == pathStart + "/META-INF".length() + && name.endsWith("/META-INF"); + } + + /** + * Returns whether a node name in the "/modules/" directory could be referencing + * a preview resource or directory under "META-INF/preview". + */ + private boolean isPreviewName(String name) { + int pathStart = name.indexOf('/', MODULES_PREFIX.length() + 1); + int previewEnd = pathStart + PREVIEW_INFIX.length(); + return pathStart > 0 + && name.regionMatches(pathStart, PREVIEW_INFIX, 0, PREVIEW_INFIX.length()) + && (name.length() == previewEnd || name.charAt(previewEnd) == '/'); + } + + private String getBaseName(ImageLocation loc) { + // Matches logic in ImageLocation#getFullName() regarding extensions. + String trailingParts = loc.getBase() + + ((loc.getExtensionOffset() != 0) ? "." + loc.getExtension() : ""); + return trailingParts.substring(trailingParts.lastIndexOf('/') + 1); + } + + /** + * Builds a link node of the form "/packages/xxx/yyy". * - *

Called by {@link #findNode(String)} if a {@code /packages/...} node - * is not present in the cache. + *

Called by {@link #findNode(String)} if a {@code /packages/...} + * node is not present in the cache (the name is not trusted). */ - private Node buildPackagesNode(String name) { - // There are only locations for the root "/packages" or "/packages/xxx" - // directories, but not the symbolic links below them (the links can be - // entirely derived from the name information in the parent directory). - // However, unlike resources this means that we do not have a constant - // time lookup for link nodes when creating them. - int packageStart = PACKAGES_ROOT.length() + 1; + private Node buildAndCacheLinkNode(String name) { + // There are only locations for "/packages" or "/packages/xxx" + // directories, but not the symbolic links below them (links are + // derived from the name information in the parent directory). + int packageStart = PACKAGES_PREFIX.length() + 1; int packageEnd = name.indexOf('/', packageStart); - if (packageEnd == -1) { - ImageLocation loc = findLocation(name); - return loc != null ? completePackageDirectory(newDirectory(name), loc) : null; - } else { - // We cannot assume that the parent directory exists for a link node, since - // the given name is untrusted and could reference a non-existent link. - // However, if the parent directory is present, we can conclude that the - // given name was not a valid link (or else it would already be cached). + // We already built the 2-level "/packages/xxx" directories, + // so if this is a 2-level name, it cannot reference a node. + if (packageEnd >= 0) { String dirName = name.substring(0, packageEnd); - if (!nodes.containsKey(dirName)) { - ImageLocation loc = findLocation(dirName); - // If the parent location doesn't exist, the link node cannot exist. - if (loc != null) { - nodes.put(dirName, completePackageDirectory(newDirectory(dirName), loc)); - // When the parent is created its child nodes are created and cached, - // but this can still return null if given name wasn't a valid link. - return nodes.get(name); + // If no parent exists here, the name cannot be valid. + Directory parent = (Directory) nodes.get(dirName); + if (parent != null) { + if (!parent.isCompleted()) { + // This caches all child links of the parent directory. + completePackageSubdirectory(parent, findLocation(dirName)); } + return nodes.get(name); } } return null; @@ -461,127 +624,118 @@ private void completeDirectory(Directory dir) { // Since the node exists, we can assert that its name starts with // either "/modules" or "/packages", making differentiation easy. // It also means that the name is valid, so it must yield a location. - assert name.startsWith(MODULES_ROOT) || name.startsWith(PACKAGES_ROOT); + assert name.startsWith(MODULES_PREFIX) || name.startsWith(PACKAGES_PREFIX); ImageLocation loc = findLocation(name); assert loc != null && name.equals(loc.getFullName()) : "Invalid location for name: " + name; - // We cannot use 'isXxxSubdirectory()' methods here since we could - // be given a top-level directory (for which that test doesn't work). - // The string MUST start "/modules" or "/packages" here. - if (name.charAt(1) == 'm') { + LocationType type = loc.getType(); + if (type == MODULES_DIR || type == MODULES_ROOT) { completeModuleDirectory(dir, loc); } else { - completePackageDirectory(dir, loc); + assert type == PACKAGES_DIR : "Invalid location type: " + loc; + completePackageSubdirectory(dir, loc); } assert dir.isCompleted() : "Directory must be complete by now: " + dir; } - /** - * Completes a modules directory by setting the list of child nodes. - * - *

The given directory can be the top level {@code /modules} directory, - * so it is NOT safe to use {@code isModulesSubdirectory(loc)} here. - */ + /** Completes a modules directory by setting the list of child nodes. */ private Directory completeModuleDirectory(Directory dir, ImageLocation loc) { assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir; - List children = createChildNodes(loc, childLoc -> { - if (isModulesSubdirectory(childLoc)) { - return nodes.computeIfAbsent(childLoc.getFullName(), this::newDirectory); + List previewOnlyNodes = getPreviewNodesToMerge(dir); + // 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); + List children = createChildNodes(loc, previewOnlyNodes.size(), childLoc -> { + LocationType type = childLoc.getType(); + if (type == MODULES_DIR) { + String name = childLoc.getFullName(); + return parentIsMetaInfDir && name.endsWith("/preview") + ? null + : nodes.computeIfAbsent(name, this::newDirectory); } else { + assert type == RESOURCE : "Invalid location type: " + loc; // Add "/modules" prefix to image location paths to get node names. String resourceName = childLoc.getFullName(true); return nodes.computeIfAbsent(resourceName, n -> newResource(n, childLoc)); } }); + children.addAll(previewOnlyNodes); dir.setChildren(children); return dir; } - /** - * Completes a package directory by setting the list of child nodes. - * - *

The given directory can be the top level {@code /packages} directory, - * so it is NOT safe to use {@code isPackagesSubdirectory(loc)} here. - */ - private Directory completePackageDirectory(Directory dir, ImageLocation loc) { + /** Completes a package directory by setting the list of child nodes. */ + private void completePackageSubdirectory(Directory dir, ImageLocation loc) { assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir; - // The only directories in the "/packages" namespace are "/packages" or - // "/packages/". However, unlike "/modules" directories, the - // location offsets mean different things. - List children; - if (dir.getName().equals(PACKAGES_ROOT)) { - // Top-level directory just contains a list of subdirectories. - children = createChildNodes(loc, c -> nodes.computeIfAbsent(c.getFullName(), this::newDirectory)); - } else { - // A package directory's content is array of offset PAIRS in the - // Strings table, but we only need the 2nd value of each pair. - IntBuffer intBuffer = getOffsetBuffer(loc); - int offsetCount = intBuffer.capacity(); - assert (offsetCount & 0x1) == 0 : "Offset count must be even: " + offsetCount; - children = new ArrayList<>(offsetCount / 2); - // Iterate the 2nd offset in each pair (odd indices). - for (int i = 1; i < offsetCount; i += 2) { - String moduleName = getString(intBuffer.get(i)); - children.add(nodes.computeIfAbsent( - dir.getName() + "/" + moduleName, - n -> newLinkNode(n, MODULES_ROOT + "/" + moduleName))); + assert !dir.isCompleted() : "Directory already completed: " + dir; + assert loc.getType() == PACKAGES_DIR : "Invalid location type: " + loc.getType(); + + // In non-preview mode we might skip a very small number of preview-only + // entries, but it's not worth "right-sizing" the array for that. + IntBuffer offsets = getOffsetBuffer(loc); + List children = new ArrayList<>(offsets.capacity() / 2); + ModuleReference.readNameOffsets(offsets, /*normal*/ true, previewMode) + .forEachRemaining(n -> { + String modName = getString(n); + Node link = newLinkNode(dir.getName() + "/" + modName, MODULES_PREFIX + "/" + modName); + children.add(ensureCached(link)); + }); + // If the parent directory exists, there must be at least one child node. + assert !children.isEmpty() : "Invalid empty package directory: " + dir; + dir.setChildren(children); + } + + /** Returns the list of child preview nodes to be merged into the given directory. */ + List getPreviewNodesToMerge(Directory dir) { + if (previewDirectoriesToMerge != null) { + Directory mergeDir = previewDirectoriesToMerge.get(dir.getName()); + if (mergeDir != null) { + return mergeDir.children; } } - // This only happens once and "completes" the directory. - dir.setChildren(children); - return dir; + return Collections.emptyList(); } /** - * Creates the list of child nodes for a {@code Directory} based on a given + * Creates the list of child nodes for a modules {@code Directory} from + * its parent location. + * + *

The {@code getChildFn} may return existing cached nodes rather + * than creating them, and if newly created nodes are to be cached, + * it is the job of {@code getChildFn}, or the caller of this method, + * to do that. * - *

Note: This cannot be used for package subdirectories as they have - * child offsets stored differently to other directories. + * @param loc a location relating to a "/modules" directory. + * @param extraNodesCount a known number of preview-only child nodes + * which will be merged onto the end of the returned list later. + * @param getChildFn a function to return a node for each child location + * (or null to skip putting anything in the list). + * @return the list of the non-null child nodes, returned by + * {@code getChildFn}, in the order of the locations entries. */ - private List createChildNodes(ImageLocation loc, Function newChildFn) { + private List createChildNodes(ImageLocation loc, int extraNodesCount, Function getChildFn) { + LocationType type = loc.getType(); + assert type == MODULES_DIR || type == MODULES_ROOT : "Invalid location type: " + loc; IntBuffer offsets = getOffsetBuffer(loc); int childCount = offsets.capacity(); - List children = new ArrayList<>(childCount); + List children = new ArrayList<>(childCount + extraNodesCount); for (int i = 0; i < childCount; i++) { - children.add(newChildFn.apply(getLocation(offsets.get(i)))); + Node childNode = getChildFn.apply(getLocation(offsets.get(i))); + if (childNode != null) { + children.add(childNode); + } } return children; } /** Helper to extract the integer offset buffer from a directory location. */ private IntBuffer getOffsetBuffer(ImageLocation dir) { - assert !isResource(dir) : "Not a directory: " + dir.getFullName(); + assert dir.getType() != RESOURCE : "Not a directory: " + dir.getFullName(); byte[] offsets = getResource(dir); ByteBuffer buffer = ByteBuffer.wrap(offsets); buffer.order(getByteOrder()); return buffer.asIntBuffer(); } - /** - * Efficiently determines if an image location is a resource. - * - *

A resource must have a valid module associated with it, so its - * module offset must be non-zero, and not equal to the offsets for - * "/modules/..." or "/packages/..." entries. - */ - private boolean isResource(ImageLocation loc) { - int moduleOffset = loc.getModuleOffset(); - return moduleOffset != 0 - && moduleOffset != modulesStringOffset - && moduleOffset != packagesStringOffset; - } - - /** - * Determines if an image location is a directory in the {@code /modules} - * namespace (if so, the location name is the node name). - * - *

In jimage, every {@code ImageLocation} under {@code /modules/} is a - * directory and has the same value for {@code getModule()}, and {@code - * getModuleOffset()}. - */ - private boolean isModulesSubdirectory(ImageLocation loc) { - return loc.getModuleOffset() == modulesStringOffset; - } - /** * Creates an "incomplete" directory node with no child nodes set. * Directories need to be "completed" before they are returned by @@ -597,7 +751,6 @@ private Directory newDirectory(String name) { * In image files, resource locations are NOT prefixed by {@code /modules}. */ private Resource newResource(String name, ImageLocation loc) { - assert name.equals(loc.getFullName(true)) : "Mismatched location for resource: " + name; return new Resource(name, loc, imageFileAttributes); } @@ -829,11 +982,12 @@ public Stream getChildNames() { throw new IllegalStateException("Cannot get child nodes of an incomplete directory: " + getName()); } - private void setChildren(List children) { + private void setChildren(List children) { assert this.children == null : this + ": Cannot set child nodes twice!"; this.children = Collections.unmodifiableList(children); } } + /** * Resource node (e.g. a ".class" entry, or any other data resource). * diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageStrings.java b/src/java.base/share/classes/jdk/internal/jimage/ImageStrings.java index eea62e444de..d9a6755d0cc 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageStrings.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageStrings.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 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 @@ -33,6 +33,19 @@ * to the jimage file provided by the shipped JDK by tools running on JDK 8. */ public interface ImageStrings { + // String offset constants are useful for efficient classification + // of location entries without string comparison. These may change + // between jimage versions (they are checked during initialization). + + /** Fixed offset for the empty string in the strings table. */ + int EMPTY_STRING_OFFSET = 0; + /** Fixed offset for the string "class" in the strings table. */ + int CLASS_STRING_OFFSET = 1; + /** Fixed offset for the string "modules" in the strings table. */ + int MODULES_STRING_OFFSET = 7; + /** Fixed offset for the string "packages" in the strings table. */ + int PACKAGES_STRING_OFFSET = 15; + String get(int offset); int add(final String string); diff --git a/src/java.base/share/classes/jdk/internal/jimage/ModuleReference.java b/src/java.base/share/classes/jdk/internal/jimage/ModuleReference.java new file mode 100644 index 00000000000..d2698c82c6b --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/jimage/ModuleReference.java @@ -0,0 +1,272 @@ +/* + * 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.nio.IntBuffer; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.IntPredicate; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * Represents the module entries stored in the buffer of {@code "/packages/xxx"} + * image locations (package subdirectories). These entries use flags which are + * similar to, but distinct from, the {@link ImageLocation} flags, so + * encapsulating them here helps avoid confusion. + * + * @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 final class ModuleReference implements Comparable { + // The following flags are designed to be additive (hence "has-resources" + // rather than "is-empty", even though "isEmpty()" is whats in the API). + // API methods like "isEmpty()" and "hasPreviewVersion()" are designed to + // match the semantics of ImageLocation flags to avoid having business + // logic need to reason about two different flag regimes. + + /** If set, the associated module has resources (in normal or preview mode). */ + private static final int FLAGS_HAS_CONTENT = 0x1; + /** If set, this package exists in non-preview mode. */ + private static final int FLAGS_HAS_NORMAL_VERSION = 0x2; + /** If set, this package exists in preview mode. */ + private static final int FLAGS_HAS_PREVIEW_VERSION = 0x4; + + /** + * References are ordered with preview versions first which permits early + * exit when processing preview entries (it's reversed because the default + * order for a boolean is {@code false < true}). + */ + private static final Comparator PREVIEW_FIRST = + Comparator.comparing(ModuleReference::hasPreviewVersion).reversed() + .thenComparing(ModuleReference::name); + + /** Creates a reference for an empty package (one without content in). */ + public static ModuleReference forEmptyPackage(String moduleName, boolean isPreview) { + return new ModuleReference(moduleName, previewFlag(isPreview)); + } + + /** Creates a reference for a preview only module. */ + public static ModuleReference forResource(String moduleName, boolean isPreview) { + return new ModuleReference(moduleName, FLAGS_HAS_CONTENT | previewFlag(isPreview)); + } + + private static int previewFlag(boolean isPreview) { + return isPreview ? FLAGS_HAS_PREVIEW_VERSION : FLAGS_HAS_NORMAL_VERSION; + } + + /** Merges two references for the same module (combining their flags). */ + public ModuleReference merge(ModuleReference other) { + if (!name.equals(other.name)) { + throw new IllegalArgumentException("Cannot merge " + other + " with " + this); + } + // Because flags are additive, we can just OR them here. + return new ModuleReference(name, flags | other.flags); + } + + private final String name; + private final int flags; + + private ModuleReference(String moduleName, int flags) { + this.name = Objects.requireNonNull(moduleName); + this.flags = flags; + } + + /** Returns the module name of this reference. */ + public String name() { + return name; + } + + /** + * Returns whether the package associated with this reference contains + * resources in this reference's module. + * + *

An invariant of the module system is that while a package may exist + * under many modules, it is only non-empty in one. + */ + public boolean hasContent() { + return ((flags & FLAGS_HAS_CONTENT) != 0); + } + + /** + * Returns whether the package associated with this reference has a preview + * version (empty or otherwise) in this reference's module. + */ + public boolean hasPreviewVersion() { + return (flags & FLAGS_HAS_PREVIEW_VERSION) != 0; + } + + /** Returns whether this reference exists only in preview mode. */ + public boolean isPreviewOnly() { + return !hasNormalVersion(flags); + } + + private static boolean hasNormalVersion(int flags) { + return (flags & FLAGS_HAS_NORMAL_VERSION) != 0; + } + + @Override + public int compareTo(ModuleReference rhs) { + return PREVIEW_FIRST.compare(this, rhs); + } + + @Override + public String toString() { + return "ModuleReference{ module=" + name + ", flags=" + flags + " }"; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ModuleReference)) { + return false; + } + ModuleReference other = (ModuleReference) obj; + return name.equals(other.name) && flags == other.flags; + } + + @Override + public int hashCode() { + return Objects.hash(name, flags); + } + + /** + * Reads the content buffer of a package subdirectory to return a sequence + * of module name offsets in the jimage. + * + * @param buffer the content buffer of an {@link ImageLocation} with type + * {@link ImageLocation.LocationType#PACKAGES_DIR PACKAGES_DIR}. + * @param includeNormal whether to include name offsets for modules present + * in normal (non-preview) mode. + * @param includePreview whether to include name offsets for modules present + * in preview mode. + * @return an iterator of module name offsets. + */ + public static Iterator readNameOffsets( + IntBuffer buffer, boolean includeNormal, boolean includePreview) { + int bufferSize = buffer.capacity(); + if (bufferSize == 0 || (bufferSize & 0x1) != 0) { + throw new IllegalArgumentException("Invalid buffer size"); + } + int testFlags = (includeNormal ? FLAGS_HAS_NORMAL_VERSION : 0) + + (includePreview ? FLAGS_HAS_PREVIEW_VERSION : 0); + if (testFlags == 0) { + throw new IllegalArgumentException("Invalid flags"); + } + + return new Iterator() { + private int idx = nextIdx(0); + + int nextIdx(int idx) { + for (; idx < bufferSize; idx += 2) { + // If any of the test flags are set, include this entry. + + // Temporarily allow for *neither* flag to be set. This is what would + // be written by a 1.0 version of the jimage flag, and indicates a + // normal resource without a preview version. + // TODO: Remove the zero-check below once image writer code is updated. + int previewFlags = + buffer.get(idx) & (FLAGS_HAS_NORMAL_VERSION | FLAGS_HAS_PREVIEW_VERSION); + if (previewFlags == 0 || (previewFlags & testFlags) != 0) { + return idx; + } else if (!includeNormal) { + // Preview entries are first in the offset buffer, so we + // can exit early (by returning the end index) if we are + // only iterating preview entries, and have run out. + break; + } + } + return bufferSize; + } + + @Override + public boolean hasNext() { + return idx < bufferSize; + } + + @Override + public Integer next() { + if (idx < bufferSize) { + int nameOffset = buffer.get(idx + 1); + idx = nextIdx(idx + 2); + return nameOffset; + } + throw new NoSuchElementException(); + } + }; + } + + /** + * Writes a list of module references to a given buffer. The given references + * list is checked carefully to ensure the written buffer will be valid. + * + *

Entries are written in order, taking two integer slots per entry as + * {@code [, ]}. + * + * @param refs the references to write, correctly ordered. + * @param buffer destination buffer. + * @param nameEncoder encoder for module names. + * @throws IllegalArgumentException in the references are invalid in any way. + */ + public static void write( + List refs, IntBuffer buffer, Function nameEncoder) { + if (refs.isEmpty()) { + throw new IllegalArgumentException("References list must be non-empty"); + } + int expectedCapacity = 2 * refs.size(); + if (buffer.capacity() != expectedCapacity) { + throw new IllegalArgumentException( + "Invalid buffer capacity: expected " + expectedCapacity + ", got " + buffer.capacity()); + } + // This catches exact duplicates in the list. + refs.stream().reduce((lhs, rhs) -> { + if (lhs.compareTo(rhs) >= 0) { + throw new IllegalArgumentException("References must be strictly ordered: " + refs); + } + return rhs; + }); + // Distinct references can have the same name (but we don't allow this). + if (refs.stream().map(ModuleReference::name).distinct().count() != refs.size()) { + throw new IllegalArgumentException("Reference names must be unique: " + refs); + } + if (refs.stream().filter(ModuleReference::hasContent).count() > 1) { + throw new IllegalArgumentException("At most one reference can have content: " + refs); + } + for (ModuleReference modRef : refs) { + buffer.put(modRef.flags); + buffer.put(nameEncoder.apply(modRef.name)); + } + } +} diff --git a/src/java.base/share/classes/jdk/internal/jimage/PreviewMode.java b/src/java.base/share/classes/jdk/internal/jimage/PreviewMode.java new file mode 100644 index 00000000000..e6e54de0f2a --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/jimage/PreviewMode.java @@ -0,0 +1,88 @@ +/* + * 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.lang.reflect.InvocationTargetException; + +/** + * Specifies the preview mode used to open a jimage file via {@link ImageReader}. + * + * @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 enum PreviewMode { + /** + * Preview mode is disabled. No preview classes or resources will be available + * in this mode. + */ + DISABLED() { + @Override + boolean resolve() { + return false; + } + }, + /** + * Preview mode is enabled. If preview classes or resources exist in the jimage file, + * they will be made available. + */ + ENABLED() { + @Override + boolean resolve() { + return true; + } + }, + /** + * The preview mode of the current run-time, typically determined by the + * {@code --enable-preview} flag. + */ + FOR_RUNTIME() { + @Override + boolean resolve() { + // We want to call jdk.internal.misc.PreviewFeatures.isEnabled(), but + // is not available in older JREs, so we must look to it reflectively. + Class clazz; + try { + clazz = Class.forName("jdk.internal.misc.PreviewFeatures"); + } catch (ClassNotFoundException e) { + // It is valid and expected that the class might not exist (JDK-8). + return false; + } + try { + return (Boolean) clazz.getDeclaredMethod("isEnabled").invoke(null); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + // But if the class exists, the method must exist and be callable. + throw new ExceptionInInitializerError(e); + } + } + }; + + /** + * Resolves whether preview mode should be enabled for an {@link ImageReader}. + */ + abstract boolean resolve(); +} diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageReaderFactory.java b/src/java.base/share/classes/jdk/internal/jimage/SystemImageReader.java similarity index 56% rename from src/java.base/share/classes/jdk/internal/jimage/ImageReaderFactory.java rename to src/java.base/share/classes/jdk/internal/jimage/SystemImageReader.java index 2ecec20d6f9..38bf786e533 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageReaderFactory.java +++ b/src/java.base/share/classes/jdk/internal/jimage/SystemImageReader.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 @@ -29,14 +29,9 @@ import java.io.UncheckedIOException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; -import java.nio.file.Path; -import java.util.concurrent.ConcurrentHashMap; -import java.util.Map; -import java.util.Objects; -import java.util.function.Function; /** - * Factory to get ImageReader + * Static holder class for singleton {@link ImageReader} instance. * * @implNote This class needs to maintain JDK 8 source compatibility. * @@ -44,15 +39,13 @@ * 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 class ImageReaderFactory { - private ImageReaderFactory() {} - - private static final String JAVA_HOME = System.getProperty("java.home"); - private static final Path BOOT_MODULES_JIMAGE; +public class SystemImageReader { + private static final ImageReader SYSTEM_IMAGE_READER; static { + String javaHome = System.getProperty("java.home"); FileSystem fs; - if (ImageReaderFactory.class.getClassLoader() == null) { + if (SystemImageReader.class.getClassLoader() == null) { try { fs = (FileSystem) Class.forName("sun.nio.fs.DefaultFileSystemProvider") .getMethod("theFileSystem") @@ -63,44 +56,22 @@ private ImageReaderFactory() {} } else { fs = FileSystems.getDefault(); } - BOOT_MODULES_JIMAGE = fs.getPath(JAVA_HOME, "lib", "modules"); - } - - private static final Map readers = new ConcurrentHashMap<>(); - - /** - * Returns an {@code ImageReader} to read from the given image file - */ - public static ImageReader get(Path jimage) throws IOException { - Objects.requireNonNull(jimage); try { - return readers.computeIfAbsent(jimage, OPENER); - } catch (UncheckedIOException io) { - throw io.getCause(); + SYSTEM_IMAGE_READER = ImageReader.open(fs.getPath(javaHome, "lib", "modules"), PreviewMode.FOR_RUNTIME); + } catch (IOException e) { + throw new ExceptionInInitializerError(e); } } - private static Function OPENER = new Function() { - public ImageReader apply(Path path) { - try { - return ImageReader.open(path); - } catch (IOException io) { - throw new UncheckedIOException(io); - } - } - }; - /** - * Returns the {@code ImageReader} to read the image file in this - * run-time image. + * Returns the singleton {@code ImageReader} to read the image file in this + * run-time image. The returned instance must not be closed. * * @throws UncheckedIOException if an I/O error occurs */ - public static ImageReader getImageReader() { - try { - return get(BOOT_MODULES_JIMAGE); - } catch (IOException ioe) { - throw new UncheckedIOException(ioe); - } + public static ImageReader get() { + return SYSTEM_IMAGE_READER; } + + private SystemImageReader() {} } diff --git a/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java b/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java index c405801506f..4d739d1e200 100644 --- a/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java +++ b/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java @@ -58,11 +58,11 @@ import java.util.Collections; import java.util.HashSet; import java.util.Iterator; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; import jdk.internal.jimage.ImageReader.Node; +import jdk.internal.jimage.PreviewMode; /** * jrt file system implementation built on System jimage files. @@ -81,13 +81,34 @@ class JrtFileSystem extends FileSystem { private volatile boolean isClosable; private SystemImage image; - JrtFileSystem(JrtFileSystemProvider provider, Map env) - throws IOException - { + /** + * Special constructor for the singleton system jrt file system. This creates + * a non-closable instance, and should only be called once by {@link + * JrtFileSystemProvider}. + * + * @param provider the provider opening the file system. + */ + JrtFileSystem(JrtFileSystemProvider provider) + throws IOException { + this.provider = provider; + this.image = SystemImage.open(PreviewMode.FOR_RUNTIME); // open image file + this.isOpen = true; + // Only the system singleton jrt file system is "unclosable". + this.isClosable = false; + } + + /** + * Creates a new, non-system, instance of the jrt file system. + * + * @param provider the provider opening the file system. + * @param mode controls whether preview resources are visible. + */ + JrtFileSystem(JrtFileSystemProvider provider, PreviewMode mode) + throws IOException { this.provider = provider; - this.image = SystemImage.open(); // open image file + this.image = SystemImage.open(mode); // open image file this.isOpen = true; - this.isClosable = env != null; + this.isClosable = true; } // FileSystem method implementations diff --git a/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystemProvider.java b/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystemProvider.java index 9ee620f4137..5721aa7411e 100644 --- a/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystemProvider.java +++ b/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystemProvider.java @@ -24,6 +24,8 @@ */ package jdk.internal.jrtfs; +import jdk.internal.jimage.PreviewMode; + import java.io.*; import java.net.MalformedURLException; import java.net.URL; @@ -44,7 +46,7 @@ /** * File system provider for jrt file systems. Conditionally creates jrt fs on - * .jimage file or exploded modules directory of underlying JDK. + * a jimage file, or exploded modules directory of underlying JDK. * * @implNote This class needs to maintain JDK 8 source compatibility. * @@ -107,8 +109,22 @@ public FileSystem newFileSystem(URI uri, Map env) if (env.containsKey("java.home")) { return newFileSystem((String)env.get("java.home"), uri, env); } else { - return new JrtFileSystem(this, env); + return new JrtFileSystem(this, parsePreviewMode(env.get("previewMode"))); + } + } + + // Currently this does not support specifying "for runtime", because it is + // expected that callers creating non-standard image readers will not be + // using them to read resources for the current runtime (they would just + // use "jrt:" URLs if they were doing that). + private static PreviewMode parsePreviewMode(Object envValue) { + if (envValue instanceof Boolean && (Boolean) envValue) { + return PreviewMode.ENABLED; + } else if (envValue instanceof String && Boolean.parseBoolean((String) envValue)) { + return PreviewMode.ENABLED; } + // Default (unspecified/null or bad parameter) is to not use preview mode. + return PreviewMode.DISABLED; } private static final String JRT_FS_JAR = "jrt-fs.jar"; @@ -208,7 +224,8 @@ private FileSystem getTheFileSystem() { fs = this.theFileSystem; if (fs == null) { try { - this.theFileSystem = fs = new JrtFileSystem(this, null); + // Special constructor call for singleton instance. + this.theFileSystem = fs = new JrtFileSystem(this); } catch (IOException ioe) { throw new InternalError(ioe); } @@ -226,7 +243,7 @@ public FileSystem getFileSystem(URI uri) { } // Checks that the given file is a JrtPath - static final JrtPath toJrtPath(Path path) { + static JrtPath toJrtPath(Path path) { Objects.requireNonNull(path, "path"); if (!(path instanceof JrtPath)) { throw new ProviderMismatchException(); @@ -257,7 +274,7 @@ public void createDirectory(Path path, FileAttribute... attrs) } @Override - public final void delete(Path path) throws IOException { + public void delete(Path path) throws IOException { toJrtPath(path).delete(); } diff --git a/src/java.base/share/classes/jdk/internal/jrtfs/SystemImage.java b/src/java.base/share/classes/jdk/internal/jrtfs/SystemImage.java index b38e953a5f9..caa915f926f 100644 --- a/src/java.base/share/classes/jdk/internal/jrtfs/SystemImage.java +++ b/src/java.base/share/classes/jdk/internal/jrtfs/SystemImage.java @@ -39,6 +39,7 @@ import jdk.internal.jimage.ImageReader; import jdk.internal.jimage.ImageReader.Node; +import jdk.internal.jimage.PreviewMode; /** * @implNote This class needs to maintain JDK 8 source compatibility. @@ -54,10 +55,10 @@ abstract class SystemImage { abstract byte[] getResource(Node node) throws IOException; abstract void close() throws IOException; - static SystemImage open() throws IOException { + static SystemImage open(PreviewMode mode) throws IOException { if (modulesImageExists) { // open a .jimage and build directory structure - final ImageReader image = ImageReader.open(moduleImageFile); + final ImageReader image = ImageReader.open(moduleImageFile, mode); return new SystemImage() { @Override Node findNode(String path) throws IOException { @@ -73,6 +74,9 @@ void close() throws IOException { } }; } + + // TODO: Maybe throw if enablePreview attempted for exploded image? + if (Files.notExists(explodedModulesDir)) throw new FileSystemNotFoundException(explodedModulesDir.toString()); return new ExplodedImage(explodedModulesDir); diff --git a/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java b/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java index 370c151af84..e7184b599f2 100644 --- a/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java +++ b/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java @@ -54,7 +54,7 @@ import java.util.stream.StreamSupport; import jdk.internal.jimage.ImageReader; -import jdk.internal.jimage.ImageReaderFactory; +import jdk.internal.jimage.SystemImageReader; import jdk.internal.access.JavaNetUriAccess; import jdk.internal.access.SharedSecrets; import jdk.internal.util.StaticProperty; @@ -392,7 +392,7 @@ public byte[] generate(String algorithm) { * Holder class for the ImageReader. */ private static class SystemImage { - static final ImageReader READER = ImageReaderFactory.getImageReader(); + static final ImageReader READER = SystemImageReader.get(); static ImageReader reader() { return READER; } diff --git a/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java index 71080950b80..b2133144d2f 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java @@ -34,7 +34,7 @@ import jdk.internal.jimage.ImageReader; import jdk.internal.jimage.ImageReader.Node; -import jdk.internal.jimage.ImageReaderFactory; +import jdk.internal.jimage.SystemImageReader; import sun.net.www.ParseUtil; import sun.net.www.URLConnection; @@ -48,7 +48,7 @@ public class JavaRuntimeURLConnection extends URLConnection { // ImageReader to access resources in jimage. - private static final ImageReader READER = ImageReaderFactory.getImageReader(); + private static final ImageReader READER = SystemImageReader.get(); // The module and resource name in the URL (i.e. "jrt:/[$MODULE[/$PATH]]"). // @@ -109,9 +109,11 @@ public InputStream getInputStream() throws IOException { @Override public long getContentLengthLong() { + // Note: UncheckedIOException is thrown by the Node subclass in + // ExplodedImage (this not obvious, so worth calling out). try { return connectResourceNode().size(); - } catch (IOException ioe) { + } catch (IOException | UncheckedIOException ioe) { return -1L; } } @@ -124,6 +126,10 @@ public int getContentLength() { // Perform percent decoding of the resource name/path from the URL. private static String percentDecode(String path) throws MalformedURLException { + if (path.indexOf('%') == -1) { + // Nothing to decode (overwhelmingly common case). + return path; + } // Any additional special case decoding logic should go here. try { return ParseUtil.decode(path); diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageStringsWriter.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageStringsWriter.java index 7ba9b7db72e..b0e0f87ad31 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageStringsWriter.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageStringsWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 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 @@ -33,7 +33,6 @@ class ImageStringsWriter implements ImageStrings { private static final int NOT_FOUND = -1; - static final int EMPTY_OFFSET = 0; private final HashMap stringToOffsetMap; private final ImageStream stream; @@ -42,16 +41,17 @@ class ImageStringsWriter implements ImageStrings { this.stringToOffsetMap = new HashMap<>(); this.stream = new ImageStream(); - // Reserve 0 offset for empty string. - int offset = addString(""); - if (offset != 0) { - throw new InternalError("Empty string not offset zero"); - } + // Frequently used/special strings for which the offset is useful. + reserveString("", ImageStrings.EMPTY_STRING_OFFSET); + reserveString("class", ImageStrings.CLASS_STRING_OFFSET); + reserveString("modules", ImageStrings.MODULES_STRING_OFFSET); + reserveString("packages", ImageStrings.PACKAGES_STRING_OFFSET); + } - // Reserve 1 offset for frequently used ".class". - offset = addString("class"); - if (offset != 1) { - throw new InternalError("'class' string not offset one"); + private void reserveString(String value, int expectedOffset) { + int offset = addString(value); + if (offset != expectedOffset) { + throw new InternalError("Reserved string \"" + value + "\" not at expected offset " + expectedOffset + "[was " + offset + "]"); } } diff --git a/test/jdk/jdk/internal/jimage/JImageReadTest.java b/test/jdk/jdk/internal/jimage/JImageReadTest.java index 35fb2adb687..46e93a8996c 100644 --- a/test/jdk/jdk/internal/jimage/JImageReadTest.java +++ b/test/jdk/jdk/internal/jimage/JImageReadTest.java @@ -42,6 +42,7 @@ import jdk.internal.jimage.ImageReader; import jdk.internal.jimage.ImageLocation; +import jdk.internal.jimage.PreviewMode; import org.testng.annotations.DataProvider; import org.testng.annotations.Optional; import org.testng.annotations.Parameters; @@ -337,16 +338,16 @@ static void test4_nameTooLong() throws IOException { @Test static void test5_imageReaderEndianness() throws IOException { // Will be opened with native byte order. - try (ImageReader nativeReader = ImageReader.open(imageFile)) { + try (ImageReader nativeReader = ImageReader.open(imageFile, PreviewMode.DISABLED)) { // Just ensure something works as expected. Assert.assertNotNull(nativeReader.findNode("/")); - } catch (IOException expected) { + } catch (IOException unexpected) { Assert.fail("Reader should be openable with native byte order."); } // Reader should not be openable with the wrong byte order. ByteOrder otherOrder = ByteOrder.nativeOrder() == BIG_ENDIAN ? LITTLE_ENDIAN : BIG_ENDIAN; - Assert.assertThrows(IOException.class, () -> ImageReader.open(imageFile, otherOrder)); + Assert.assertThrows(IOException.class, () -> ImageReader.open(imageFile, otherOrder, PreviewMode.DISABLED)); } // main method to run standalone from jtreg diff --git a/test/jdk/jdk/internal/jimage/ModuleReferenceTest.java b/test/jdk/jdk/internal/jimage/ModuleReferenceTest.java new file mode 100644 index 00000000000..82b96a98e5c --- /dev/null +++ b/test/jdk/jdk/internal/jimage/ModuleReferenceTest.java @@ -0,0 +1,240 @@ +/* + * 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. + * + * 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. + */ + +import jdk.internal.jimage.ModuleReference; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; + +import static java.util.function.Predicate.not; +import static jdk.internal.jimage.ModuleReference.forEmptyPackage; +import static jdk.internal.jimage.ModuleReference.forResource; +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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/* + * @test + * @summary Tests for ModuleReference. + * @modules java.base/jdk.internal.jimage + * @run junit/othervm -esa ModuleReferenceTest + */ +public final class ModuleReferenceTest { + // Copied (not referenced) for testing. + private static final int FLAGS_HAS_CONTENT = 0x1; + private static final int FLAGS_HAS_NORMAL_VERSION = 0x2; + private static final int FLAGS_HAS_PREVIEW_VERSION = 0x4; + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void emptyRefs(boolean isPreview) { + ModuleReference ref = forEmptyPackage("module", isPreview); + + assertEquals("module", ref.name()); + assertFalse(ref.hasContent()); + assertEquals(isPreview, ref.hasPreviewVersion()); + assertEquals(isPreview, ref.isPreviewOnly()); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void resourceRefs(boolean isPreview) { + ModuleReference ref = forResource("module", isPreview); + + assertEquals("module", ref.name()); + assertTrue(ref.hasContent()); + assertEquals(isPreview, ref.hasPreviewVersion()); + assertEquals(isPreview, ref.isPreviewOnly()); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void mergedRefs(boolean isPreview) { + ModuleReference emptyRef = forEmptyPackage("module", true); + ModuleReference resourceRef = forResource("module", isPreview); + ModuleReference merged = emptyRef.merge(resourceRef); + + // Merging preserves whether there's content. + assertTrue(merged.hasContent()); + // And clears the preview-only status unless it was set in both. + assertEquals(isPreview, merged.isPreviewOnly()); + } + + @Test + public void writeBuffer() { + List refs = Arrays.asList( + forEmptyPackage("alpha", true), + forEmptyPackage("beta", false).merge(forEmptyPackage("beta", true)), + forResource("gamma", false), + forEmptyPackage("zeta", false)); + IntBuffer buffer = IntBuffer.allocate(2 * refs.size()); + ModuleReference.write(refs, buffer, testEncoder()); + assertArrayEquals( + new int[]{ + FLAGS_HAS_PREVIEW_VERSION, 100, + FLAGS_HAS_NORMAL_VERSION | FLAGS_HAS_PREVIEW_VERSION, 101, + FLAGS_HAS_NORMAL_VERSION | FLAGS_HAS_CONTENT, 102, + FLAGS_HAS_NORMAL_VERSION, 103}, + buffer.array()); + } + + @Test + public void writeBuffer_emptyList() { + IntBuffer buffer = IntBuffer.allocate(0); + var err = assertThrows( + IllegalArgumentException.class, + () -> ModuleReference.write(List.of(), buffer, null)); + assertTrue(err.getMessage().contains("non-empty")); + } + + @Test + public void writeBuffer_badCapacity() { + List refs = Arrays.asList( + forResource("first", false), + forEmptyPackage("alpha", false)); + IntBuffer buffer = IntBuffer.allocate(10); + var err = assertThrows( + IllegalArgumentException.class, + () -> ModuleReference.write(refs, buffer, null)); + assertTrue(err.getMessage().contains("buffer capacity")); + } + + @Test + public void writeBuffer_multipleContent() { + // Only one module reference (at most) can have resources. + List refs = Arrays.asList( + forResource("alpha", false), + forResource("beta", false)); + IntBuffer buffer = IntBuffer.allocate(2 * refs.size()); + var err = assertThrows( + IllegalArgumentException.class, + () -> ModuleReference.write(refs, buffer, null)); + assertTrue(err.getMessage().contains("content")); + } + + @Test + public void writeBuffer_badOrdering() { + // Badly ordered because preview references should come first. + List refs = Arrays.asList( + forEmptyPackage("alpha", false), + forEmptyPackage("beta", true)); + IntBuffer buffer = IntBuffer.allocate(2 * refs.size()); + var err = assertThrows( + IllegalArgumentException.class, + () -> ModuleReference.write(refs, buffer, null)); + assertTrue(err.getMessage().contains("strictly ordered")); + } + + @Test + public void writeBuffer_duplicateRef() { + // Technically distinct, and correctly sorted, but with duplicate names. + List refs = Arrays.asList( + forEmptyPackage("duplicate", true), + forEmptyPackage("duplicate", false)); + IntBuffer buffer = IntBuffer.allocate(2 * refs.size()); + var err = assertThrows( + IllegalArgumentException.class, + () -> ModuleReference.write(refs, buffer, null)); + assertTrue(err.getMessage().contains("unique")); + } + + @Test + public void readNameOffsets() { + // Preview versions must be first (important for early exit). + IntBuffer buffer = IntBuffer.wrap(new int[]{ + FLAGS_HAS_NORMAL_VERSION | FLAGS_HAS_PREVIEW_VERSION, 100, + FLAGS_HAS_PREVIEW_VERSION, 101, + FLAGS_HAS_NORMAL_VERSION | FLAGS_HAS_CONTENT, 102, + FLAGS_HAS_NORMAL_VERSION, 103}); + + List normalOffsets = asList(ModuleReference.readNameOffsets(buffer, true, false)); + List previewOffsets = asList(ModuleReference.readNameOffsets(buffer, false, true)); + List allOffsets = asList(ModuleReference.readNameOffsets(buffer, true, true)); + + assertEquals(List.of(100, 102, 103), normalOffsets); + assertEquals(List.of(100, 101), previewOffsets); + assertEquals(List.of(100, 101, 102, 103), allOffsets); + } + + @Test + public void readNameOffsets_badBufferSize() { + var err = assertThrows( + IllegalArgumentException.class, + () -> ModuleReference.readNameOffsets(IntBuffer.allocate(3), true, false)); + assertTrue(err.getMessage().contains("buffer size")); + } + + @Test + public void readNameOffsets_badFlags() { + IntBuffer buffer = IntBuffer.wrap(new int[]{FLAGS_HAS_CONTENT, 100}); + var err = assertThrows( + IllegalArgumentException.class, + () -> ModuleReference.readNameOffsets(buffer, false, false)); + assertTrue(err.getMessage().contains("flags")); + } + + @Test + public void sortOrder_previewFirst() { + List refs = Arrays.asList( + forEmptyPackage("normal.beta", false), + forResource("preview.beta", true), + forEmptyPackage("preview.alpha", true), + forEmptyPackage("normal.alpha", false)); + refs.sort(Comparator.naturalOrder()); + // Non-empty first with remaining sorted by name. + assertEquals( + List.of("preview.alpha", "preview.beta", "normal.alpha", "normal.beta"), + refs.stream().map(ModuleReference::name).toList()); + } + + private static List asList(Iterator src) { + List list = new ArrayList<>(); + src.forEachRemaining(list::add); + return list; + } + + // Encodes strings sequentially starting from index 100. + private static Function testEncoder() { + List cache = new ArrayList<>(); + return s -> { + int i = cache.indexOf(s); + if (i == -1) { + cache.add(s); + return 100 + (cache.size() - 1); + } else { + return 100 + i; + } + }; + } +} diff --git a/test/jdk/tools/jimage/ImageReaderDuplicateChildNodesTest.java b/test/jdk/tools/jimage/ImageReaderDuplicateChildNodesTest.java index 3eed3bf972c..bf0855f273f 100644 --- a/test/jdk/tools/jimage/ImageReaderDuplicateChildNodesTest.java +++ b/test/jdk/tools/jimage/ImageReaderDuplicateChildNodesTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 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 @@ -54,7 +54,7 @@ public static void main(final String[] args) throws Exception { System.out.println("Running test against image " + imagePath); final String integersParentResource = "/modules/java.base/java/lang"; final String integerResource = integersParentResource + "/Integer.class"; - try (final ImageReader reader = ImageReader.open(imagePath)) { + try (final ImageReader reader = ImageReader.open(imagePath, /* previewMode */ false)) { // find the child node/resource first final ImageReader.Node integerNode = reader.findNode(integerResource); if (integerNode == null) { diff --git a/test/micro/org/openjdk/bench/jdk/internal/jrtfs/ImageReaderBenchmark.java b/test/micro/org/openjdk/bench/jdk/internal/jrtfs/ImageReaderBenchmark.java index 4f97e12171f..89952805417 100644 --- a/test/micro/org/openjdk/bench/jdk/internal/jrtfs/ImageReaderBenchmark.java +++ b/test/micro/org/openjdk/bench/jdk/internal/jrtfs/ImageReaderBenchmark.java @@ -24,6 +24,7 @@ import jdk.internal.jimage.ImageReader; import jdk.internal.jimage.ImageReader.Node; +import jdk.internal.jimage.PreviewMode; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -39,7 +40,6 @@ import org.openjdk.jmh.infra.Blackhole; import java.io.IOException; -import java.nio.ByteOrder; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -71,12 +71,10 @@ public class ImageReaderBenchmark { /// any lifetime annotations that are needed. static class BaseState { protected Path copiedImageFile; - protected ByteOrder byteOrder; long count = 0; public void setUp() throws IOException { copiedImageFile = Files.createTempFile("copied_jimage", ""); - byteOrder = ByteOrder.nativeOrder(); Files.copy(SYSTEM_IMAGE_FILE, copiedImageFile, REPLACE_EXISTING); } @@ -93,7 +91,7 @@ public static class WarmStartWithImageReader extends BaseState { @Setup(Level.Trial) public void setUp() throws IOException { super.setUp(); - reader = ImageReader.open(copiedImageFile, byteOrder); + reader = ImageReader.open(copiedImageFile, PreviewMode.DISABLED); } @TearDown(Level.Trial) @@ -122,7 +120,7 @@ public static class ColdStartWithImageReader extends BaseState { @Setup(Level.Iteration) public void setup() throws IOException { super.setUp(); - reader = ImageReader.open(copiedImageFile, byteOrder); + reader = ImageReader.open(copiedImageFile, PreviewMode.DISABLED); } @TearDown(Level.Iteration) @@ -149,7 +147,7 @@ public void warmCache_CountAllNodes(WarmStartWithImageReader state) throws IOExc @Benchmark @BenchmarkMode(Mode.SingleShotTime) public void coldStart_InitAndCount(ColdStart state) throws IOException { - try (var reader = ImageReader.open(state.copiedImageFile, state.byteOrder)) { + try (var reader = ImageReader.open(state.copiedImageFile, PreviewMode.DISABLED)) { state.count = countAllNodes(reader, reader.findNode("/")); } } @@ -173,7 +171,7 @@ public void coldStart_CountOnly(ColdStartWithImageReader state) throws IOExcepti @BenchmarkMode(Mode.SingleShotTime) public void coldStart_LoadJavacInitClasses(Blackhole bh, ColdStart state) throws IOException { int errors = 0; - try (var reader = ImageReader.open(state.copiedImageFile, state.byteOrder)) { + try (var reader = ImageReader.open(state.copiedImageFile, PreviewMode.DISABLED)) { for (String path : INIT_CLASSES) { // Path determination isn't perfect so there can be a few "misses" in here. // Report the count of bad paths as the "result", which should be < 20 or so. @@ -210,7 +208,7 @@ static long countAllNodes(ImageReader reader, Node node) { // DO NOT run this before the benchmark, as it will cache all the nodes! private static void reportMissingClassesAndFail(ColdStart state, int errors) throws IOException { List missing = new ArrayList<>(errors); - try (var reader = ImageReader.open(state.copiedImageFile, state.byteOrder)) { + try (var reader = ImageReader.open(state.copiedImageFile, PreviewMode.DISABLED)) { for (String path : INIT_CLASSES) { if (reader.findNode(path) == null) { missing.add(path); @@ -507,7 +505,7 @@ private static void reportMissingClassesAndFail(ColdStart state, int errors) thr "/modules/java.base/java/util/function/BiFunction.class", "/modules/java.base/jdk/internal/access/JavaNioAccess.class", "/modules/java.base/java/nio/HeapByteBuffer.class", - "/modules/java.base/java/nio/ByteOrder.class", + "/modules/java.base/java/nio/PreviewMode.DISABLED.class", "/modules/java.base/java/io/BufferedWriter.class", "/modules/java.base/java/lang/Terminator.class", "/modules/java.base/jdk/internal/misc/Signal$Handler.class",