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:
+ *
+ * - src/java.base/share/native/libjimage/imageFile.hpp
+ *
+ *
+ * Version history:
+ *
+ * - {@code 1.0}: Original version.
+ *
- {@code 1.1}: Support preview mode with new flags.
+ *
+ *
* @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 extends Node> 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",