8377070: Update jimage format to support classes compiled with preview feature enabled

Co-authored-by: David Beaumont <dbeaumont@openjdk.org>
Reviewed-by: jpai, coleenp, sgehwolf
This commit is contained in:
Alan Bateman 2026-05-12 10:09:28 +00:00
parent bbd5f6d877
commit 4edfc387f1
38 changed files with 2719 additions and 990 deletions

View File

@ -96,9 +96,18 @@ static JImageClose_t JImageClose = nullptr;
static JImageFindResource_t JImageFindResource = nullptr;
static JImageGetResource_t JImageGetResource = nullptr;
// JimageFile pointer, or null if exploded JDK build.
// JImageFile pointer, or null if exploded JDK build.
static JImageFile* JImage_file = nullptr;
// PreviewMode status to control preview behaviour. JImage_file is unusable
// for normal lookup until (Preview_mode != PREVIEW_MODE_UNINITIALIZED).
enum PreviewMode {
PREVIEW_MODE_UNINITIALIZED = 0,
PREVIEW_MODE_DEFAULT = 1,
PREVIEW_MODE_ENABLE_PREVIEW = 2
};
static PreviewMode Preview_mode = PREVIEW_MODE_UNINITIALIZED;
// Globals
PerfCounter* ClassLoader::_perf_accumulated_time = nullptr;
@ -154,7 +163,7 @@ void ClassLoader::print_counters(outputStream *st) {
GrowableArray<ModuleClassPathList*>* ClassLoader::_patch_mod_entries = nullptr;
GrowableArray<ModuleClassPathList*>* ClassLoader::_exploded_entries = nullptr;
ClassPathEntry* ClassLoader::_jrt_entry = nullptr;
ClassPathImageEntry* ClassLoader::_jrt_entry = nullptr;
ClassPathEntry* volatile ClassLoader::_first_append_entry_list = nullptr;
ClassPathEntry* volatile ClassLoader::_last_append_entry = nullptr;
@ -171,15 +180,6 @@ static bool string_starts_with(const char* str, const char* str_to_find) {
}
#endif
static const char* get_jimage_version_string() {
static char version_string[10] = "";
if (version_string[0] == '\0') {
jio_snprintf(version_string, sizeof(version_string), "%d.%d",
VM_Version::vm_major_version(), VM_Version::vm_minor_version());
}
return (const char*)version_string;
}
bool ClassLoader::string_ends_with(const char* str, const char* str_to_find) {
size_t str_len = strlen(str);
size_t str_to_find_len = strlen(str_to_find);
@ -234,6 +234,69 @@ Symbol* ClassLoader::package_from_class_name(const Symbol* name, bool* bad_class
return SymbolTable::new_symbol(name, pointer_delta_as_int(start, base), pointer_delta_as_int(end, base));
}
// --------------------------------
// The following jimage_xxx static functions encapsulate all JImage_file and Preview_mode access.
// This is done to make it easy to reason about the JImage file state (exists vs initialized etc.).
// Opens the named JImage file and sets the JImage file reference.
// Returns true if opening the JImage file was successful (see also jimage_is_open()).
static bool jimage_open(const char* modules_path) {
// Currently 'error' is not set to anything useful, so ignore it here.
jint error;
JImage_file = (*JImageOpen)(modules_path, &error);
if (Arguments::has_jimage() && JImage_file == nullptr) {
// The modules file exists but is unreadable or corrupt
vm_exit_during_initialization(err_msg("Unable to load %s", modules_path));
}
return JImage_file != nullptr;
}
// Closes and clears the JImage file reference (this will only be called during shutdown).
static void jimage_close() {
if (JImage_file != nullptr) {
(*JImageClose)(JImage_file);
JImage_file = nullptr;
}
}
// Returns whether a JImage file was opened (but NOT whether it was initialized yet).
static bool jimage_is_open() {
return JImage_file != nullptr;
}
// Returns the JImage file reference (which may or may not be initialized).
static JImageFile* jimage_non_null() {
assert(jimage_is_open(), "should have been opened by ClassLoader::lookup_vm_options "
"and remains open throughout normal JVM lifetime");
return JImage_file;
}
// Returns true if jimage_init() has been called. Once the JImage file is initialized,
// jimage_is_preview_enabled() can be called to correctly determine the access mode.
static bool jimage_is_initialized() {
return jimage_is_open() && Preview_mode != PREVIEW_MODE_UNINITIALIZED;
}
// Returns the access mode for an initialized JImage file (reflects --enable-preview).
static bool is_preview_enabled() {
return Preview_mode == PREVIEW_MODE_ENABLE_PREVIEW;
}
// Looks up the location of a named JImage resource. This "raw" lookup function allows
// the preview mode to be manually specified, so must not be accessible outside this
// class. ClassPathImageEntry manages all calls for resources after startup is complete.
static JImageLocationRef jimage_find_resource(const char* module_name,
const char* file_name,
bool is_preview,
jlong* size) {
return ((*JImageFindResource)(jimage_non_null(),
module_name,
file_name,
is_preview,
size));
}
// --------------------------------
// Given a fully qualified package name, find its defining package in the class loader's
// package entry table.
PackageEntry* ClassLoader::get_package_entry(Symbol* pkg_name, ClassLoaderData* loader_data) {
@ -372,28 +435,15 @@ ClassFileStream* ClassPathZipEntry::open_stream(JavaThread* current, const char*
DEBUG_ONLY(ClassPathImageEntry* ClassPathImageEntry::_singleton = nullptr;)
JImageFile* ClassPathImageEntry::jimage() const {
return JImage_file;
}
JImageFile* ClassPathImageEntry::jimage_non_null() const {
assert(ClassLoader::has_jrt_entry(), "must be");
assert(jimage() != nullptr, "should have been opened by ClassLoader::lookup_vm_options "
"and remained throughout normal JVM lifetime");
return jimage();
}
void ClassPathImageEntry::close_jimage() {
if (jimage() != nullptr) {
(*JImageClose)(jimage());
JImage_file = nullptr;
}
jimage_close();
}
ClassPathImageEntry::ClassPathImageEntry(JImageFile* jimage, const char* name) :
ClassPathImageEntry::ClassPathImageEntry(const char* name) :
ClassPathEntry() {
guarantee(jimage != nullptr, "jimage file is null");
guarantee(jimage_is_initialized(), "jimage is not initialized");
guarantee(name != nullptr, "jimage file name is null");
assert(_singleton == nullptr, "VM supports only one jimage");
DEBUG_ONLY(_singleton = this);
size_t len = strlen(name) + 1;
@ -412,6 +462,8 @@ ClassFileStream* ClassPathImageEntry::open_stream(JavaThread* current, const cha
// 2. A package is in at most one module in the jimage file.
//
ClassFileStream* ClassPathImageEntry::open_stream_for_loader(JavaThread* current, const char* name, ClassLoaderData* loader_data) {
const bool is_preview = is_preview_enabled();
jlong size;
JImageLocationRef location = 0;
@ -420,7 +472,7 @@ ClassFileStream* ClassPathImageEntry::open_stream_for_loader(JavaThread* current
if (pkg_name != nullptr) {
if (!Universe::is_module_initialized()) {
location = (*JImageFindResource)(jimage_non_null(), JAVA_BASE_NAME, get_jimage_version_string(), name, &size);
location = jimage_find_resource(JAVA_BASE_NAME, name, is_preview, &size);
} else {
PackageEntry* package_entry = ClassLoader::get_package_entry(pkg_name, loader_data);
if (package_entry != nullptr) {
@ -431,7 +483,7 @@ ClassFileStream* ClassPathImageEntry::open_stream_for_loader(JavaThread* current
assert(module->is_named(), "Boot classLoader package is in unnamed module");
const char* module_name = module->name()->as_C_string();
if (module_name != nullptr) {
location = (*JImageFindResource)(jimage_non_null(), module_name, get_jimage_version_string(), name, &size);
location = jimage_find_resource(module_name, name, is_preview, &size);
}
}
}
@ -444,7 +496,7 @@ ClassFileStream* ClassPathImageEntry::open_stream_for_loader(JavaThread* current
char* data = NEW_RESOURCE_ARRAY(char, size);
(*JImageGetResource)(jimage_non_null(), location, data, size);
// Resource allocated
assert(this == (ClassPathImageEntry*)ClassLoader::get_jrt_entry(), "must be");
assert(this == ClassLoader::get_jrt_entry(), "must be");
return new ClassFileStream((u1*)data,
checked_cast<int>(size),
_name,
@ -454,16 +506,9 @@ ClassFileStream* ClassPathImageEntry::open_stream_for_loader(JavaThread* current
return nullptr;
}
JImageLocationRef ClassLoader::jimage_find_resource(JImageFile* jf,
const char* module_name,
const char* file_name,
jlong &size) {
return ((*JImageFindResource)(jf, module_name, get_jimage_version_string(), file_name, &size));
}
bool ClassPathImageEntry::is_modules_image() const {
assert(this == _singleton, "VM supports a single jimage");
assert(this == (ClassPathImageEntry*)ClassLoader::get_jrt_entry(), "must be used for jrt entry");
assert(this == ClassLoader::get_jrt_entry(), "must be used for jrt entry");
return true;
}
@ -618,14 +663,15 @@ void ClassLoader::setup_bootstrap_search_path_impl(JavaThread* current, const ch
struct stat st;
if (os::stat(path, &st) == 0) {
// Directory found
if (JImage_file != nullptr) {
if (jimage_is_open()) {
assert(Arguments::has_jimage(), "sanity check");
const char* canonical_path = get_canonical_path(path, current);
assert(canonical_path != nullptr, "canonical_path issue");
_jrt_entry = new ClassPathImageEntry(JImage_file, canonical_path);
// Hand over lifecycle control of the JImage file to the _jrt_entry singleton
// (see ClassPathImageEntry::close_jimage). The image must be initialized by now.
_jrt_entry = new ClassPathImageEntry(canonical_path);
assert(_jrt_entry != nullptr && _jrt_entry->is_modules_image(), "No java runtime image present");
assert(_jrt_entry->jimage() != nullptr, "No java runtime image");
} // else it's an exploded build.
} else {
// If path does not exist, exit
@ -645,7 +691,7 @@ void ClassLoader::setup_bootstrap_search_path_impl(JavaThread* current, const ch
static const char* get_exploded_module_path(const char* module_name, bool c_heap) {
const char *home = Arguments::get_java_home();
const char file_sep = os::file_separator()[0];
// 10 represents the length of "modules" + 2 file separators + \0
// 10 represents the length of "modules" (7) + 2 file separators + \0
size_t len = strlen(home) + strlen(module_name) + 10;
char *path = c_heap ? NEW_C_HEAP_ARRAY(char, len, mtModule) : NEW_RESOURCE_ARRAY(char, len);
jio_snprintf(path, len, "%s%cmodules%c%s", home, file_sep, file_sep, module_name);
@ -1398,20 +1444,8 @@ void ClassLoader::initialize(TRAPS) {
setup_bootstrap_search_path(THREAD);
}
static char* lookup_vm_resource(JImageFile *jimage, const char *jimage_version, const char *path) {
jlong size;
JImageLocationRef location = (*JImageFindResource)(jimage, "java.base", jimage_version, path, &size);
if (location == 0)
return nullptr;
char *val = NEW_C_HEAP_ARRAY(char, size+1, mtClass);
(*JImageGetResource)(jimage, location, val, size);
val[size] = '\0';
return val;
}
// Lookup VM options embedded in the modules jimage file
char* ClassLoader::lookup_vm_options() {
jint error;
char modules_path[JVM_MAXPATHLEN];
const char* fileSep = os::file_separator();
@ -1419,32 +1453,41 @@ char* ClassLoader::lookup_vm_options() {
load_jimage_library();
jio_snprintf(modules_path, JVM_MAXPATHLEN, "%s%slib%smodules", Arguments::get_java_home(), fileSep, fileSep);
JImage_file =(*JImageOpen)(modules_path, &error);
if (JImage_file == nullptr) {
if (Arguments::has_jimage()) {
// The modules file exists but is unreadable or corrupt
vm_exit_during_initialization(err_msg("Unable to load %s", modules_path));
if (jimage_open(modules_path)) {
// Special case where we lookup the options string *before* set_preview_mode() is called.
// Since VM arguments have not been parsed, and the ClassPathImageEntry singleton
// has not been created yet, we access the JImage file directly in non-preview mode.
jlong size;
JImageLocationRef location =
jimage_find_resource(JAVA_BASE_NAME, "jdk/internal/vm/options", /* is_preview */ false, &size);
if (location != 0) {
char* options = NEW_C_HEAP_ARRAY(char, size+1, mtClass);
(*JImageGetResource)(jimage_non_null(), location, options, size);
options[size] = '\0';
return options;
}
return nullptr;
}
return nullptr;
}
const char *jimage_version = get_jimage_version_string();
char *options = lookup_vm_resource(JImage_file, jimage_version, "jdk/internal/vm/options");
return options;
// Finishes initializing the JImageFile (if present) by setting the access mode.
void ClassLoader::set_preview_mode(bool enable_preview) {
assert(Preview_mode == PREVIEW_MODE_UNINITIALIZED, "set_preview_mode must not be called twice");
Preview_mode = enable_preview ? PREVIEW_MODE_ENABLE_PREVIEW : PREVIEW_MODE_DEFAULT;
}
bool ClassLoader::is_module_observable(const char* module_name) {
assert(JImageOpen != nullptr, "jimage library should have been opened");
if (JImage_file == nullptr) {
if (!jimage_is_open()) {
struct stat st;
const char *path = get_exploded_module_path(module_name, true);
bool res = os::stat(path, &st) == 0;
FREE_C_HEAP_ARRAY(path);
return res;
}
// We don't expect preview mode (i.e. --enable-preview) to affect module visibility.
jlong size;
const char *jimage_version = get_jimage_version_string();
return (*JImageFindResource)(JImage_file, module_name, jimage_version, "module-info.class", &size) != 0;
return jimage_find_resource(module_name, "module-info.class", /* is_preview */ false, &size) != 0;
}
jlong ClassLoader::classloader_time_ms() {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1997, 2026, 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
@ -99,7 +99,8 @@ class ClassPathZipEntry: public ClassPathEntry {
};
// For java image files
// A singleton path entry which takes ownership of the initialized JImageFile
// reference. Not used for exploded builds.
class ClassPathImageEntry: public ClassPathEntry {
private:
const char* _name;
@ -107,11 +108,12 @@ private:
public:
bool is_modules_image() const;
const char* name() const { return _name == nullptr ? "" : _name; }
JImageFile* jimage() const;
JImageFile* jimage_non_null() const;
// Called to close the JImage during os::abort (normally not called).
void close_jimage();
ClassPathImageEntry(JImageFile* jimage, const char* name);
// Takes effective ownership of the static JImageFile pointer.
ClassPathImageEntry(const char* name);
virtual ~ClassPathImageEntry() { ShouldNotReachHere(); }
ClassFileStream* open_stream(JavaThread* current, const char* name);
ClassFileStream* open_stream_for_loader(JavaThread* current, const char* name, ClassLoaderData* loader_data);
};
@ -201,10 +203,10 @@ class ClassLoader: AllStatic {
static GrowableArray<ModuleClassPathList*>* _patch_mod_entries;
// 2. the base piece
// Contains the ClassPathEntry of the modular java runtime image.
// Contains the ClassPathImageEntry of the modular java runtime image.
// If no java runtime image is present, this indicates a
// build with exploded modules is being used instead.
static ClassPathEntry* _jrt_entry;
static ClassPathImageEntry* _jrt_entry;
static GrowableArray<ModuleClassPathList*>* _exploded_entries;
enum { EXPLODED_ENTRY_SIZE = 80 }; // Initial number of exploded modules
@ -354,15 +356,20 @@ class ClassLoader: AllStatic {
static void append_boot_classpath(ClassPathEntry* new_entry);
#endif
// Retrieves additional VM options prior to flags processing. Options held
// in the JImage file are retrieved without fully initializing it. (this is
// the only JImage lookup which can succeed before init_jimage() is called).
static char* lookup_vm_options();
// Called once, after all flags are processed, to finish initializing the
// JImage file. Until this is called, jimage_find_resource(), and any other
// JImage resource lookups or access will fail.
static void set_preview_mode(bool enable_preview);
// Determines if the named module is present in the
// modules jimage file or in the exploded modules directory.
static bool is_module_observable(const char* module_name);
static JImageLocationRef jimage_find_resource(JImageFile* jf, const char* module_name,
const char* file_name, jlong &size);
static void trace_class_path(const char* msg, const char* name = nullptr);
// VM monitoring and management support

View File

@ -2719,6 +2719,10 @@ jint Arguments::finalize_vm_init_args() {
return JNI_ERR;
}
// Called after ClassLoader::lookup_vm_options() but before class loading begins.
// TODO: Obtain and pass correct preview mode flag value here.
ClassLoader::set_preview_mode(false);
if (!check_vm_args_consistency()) {
return JNI_ERR;
}

View File

@ -37,8 +37,11 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import jdk.internal.jimage.decompressor.Decompressor;
/**
@ -316,6 +319,56 @@ public class BasicImageReader implements AutoCloseable {
.toArray(String[]::new);
}
/**
* Returns the "raw" API for accessing underlying jimage resource entries.
*
* <p>This is only meaningful for use by code dealing directly with jimage
* files, and cannot be used to reliably lookup resources used at runtime.
*
* <p>The returned {@code ResourceEntries} remains valid until the image
* reader from which it was obtained is closed.
*/
// Package visible for use by ImageReader.
ResourceEntries getResourceEntries() {
return new ResourceEntries() {
@Override
public Stream<String> getEntryNames(String module) {
if (module.isEmpty() || module.equals("modules") || module.equals("packages")) {
throw new IllegalArgumentException("Invalid module name: " + module);
}
return IntStream.range(0, offsets.capacity())
.map(offsets::get)
.filter(offset -> offset != 0)
// Reusing a location instance or getting the module
// offset directly would save a lot of allocations here.
.mapToObj(offset -> ImageLocation.readFrom(BasicImageReader.this, offset))
// Reverse lookup of module offset would be faster here.
.filter(loc -> module.equals(loc.getModule()))
.map(ImageLocation::getFullName);
}
private ImageLocation getResourceLocation(String name) {
if (!name.startsWith("/modules/") && !name.startsWith("/packages/")) {
ImageLocation location = BasicImageReader.this.findLocation(name);
if (location != null) {
return location;
}
}
throw new NoSuchElementException("No such resource entry: " + name);
}
@Override
public long getSize(String name) {
return getResourceLocation(name).getUncompressedSize();
}
@Override
public byte[] getBytes(String name) {
return BasicImageReader.this.getResource(getResourceLocation(name));
}
};
}
ImageLocation getLocation(int offset) {
return ImageLocation.readFrom(this, offset);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2026, 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.nio.IntBuffer;
import java.util.Objects;
/**
* Defines the header and version information for jimage files.
*
* <p>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.
*
* <p>Known jimage file code which needs updating on version change:
* <ul>
* <li>src/java.base/share/native/libjimage/imageFile.hpp
* </ul>
*
* <p>Version history:
* <ul>
* <li>{@code 1.0}: Original version.
* <li>{@code 1.1}: Support preview mode with new flags.
* </ul>
*
* @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 @@ import java.util.Objects;
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;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2026, 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
@ -26,7 +26,9 @@
package jdk.internal.jimage;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
/**
* @implNote This class needs to maintain JDK 8 source compatibility.
@ -36,15 +38,172 @@ import java.util.Objects;
* to the jimage file provided by the shipped JDK by tools running on JDK 8.
*/
public class ImageLocation {
// Also defined in src/java.base/share/native/libjimage/imageFile.hpp
/** End of attribute stream marker. */
public static final int ATTRIBUTE_END = 0;
/** String table offset of module name. */
public static final int ATTRIBUTE_MODULE = 1;
/** String table offset of resource path parent. */
public static final int ATTRIBUTE_PARENT = 2;
/** String table offset of resource path base. */
public static final int ATTRIBUTE_BASE = 3;
/** String table offset of resource path extension. */
public static final int ATTRIBUTE_EXTENSION = 4;
/** Container byte offset of resource. */
public static final int ATTRIBUTE_OFFSET = 5;
/** In-image byte size of the compressed resource. */
public static final int ATTRIBUTE_COMPRESSED = 6;
/** In-memory byte size of the uncompressed resource. */
public static final int ATTRIBUTE_UNCOMPRESSED = 7;
public static final int ATTRIBUTE_COUNT = 8;
/** Flags relating to preview mode resources. */
public static final int ATTRIBUTE_PREVIEW_FLAGS = 8;
/** Number of attribute kinds. */
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.
*
* <p>This can apply to both resources and directories in the
* {@code /modules/xxx/...} namespace, as well as {@code /packages/xxx}
* directories.
*
* <p>For {@code /packages/xxx} directories, it indicates that the package
* has preview resources in one of the modules in which it exists.
*/
private static final int FLAGS_HAS_PREVIEW_VERSION = 0x1;
/**
* Set on all locations in the {@code /modules/xxx/META-INF/preview/...}
* namespace.
*
* <p>This flag is mutually exclusive with {@link #FLAGS_HAS_PREVIEW_VERSION}.
*/
private static final int FLAGS_IS_PREVIEW_VERSION = 0x2;
/**
* Indicates that a location only exists due to preview resources.
*
* <p>This can apply to both resources and directories in the
* {@code /modules/xxx/...} namespace, as well as {@code /packages/xxx}
* directories.
*
* <p>For {@code /packages/xxx} directories it indicates that, for every
* module in which the package exists, it is preview only.
*
* <p>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).
*/
private static final int FLAGS_IS_PREVIEW_ONLY = 0x4;
// 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).
*
* <p>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.
*
* <p>Based on the entry name, the flags are:
* <ul>
* <li>{@code "[/modules]/<module>/<path>"} normal resource or directory:<br>
* Zero, or {@code FLAGS_HAS_PREVIEW_VERSION} if a preview entry exists.
* <li>{@code "[/modules]/<module>/META-INF/preview/<path>"} preview
* resource or directory:<br>
* {@code FLAGS_IS_PREVIEW_VERSION}, and additionally {@code
* FLAGS_IS_PREVIEW_ONLY} if no normal version of the resource or
* directory exists.
* <li>In all other cases, returned flags are zero (note that {@code
* "/packages/xxx"} entries may have flags, but these are calculated
* elsewhere).
* </ul>
*
* @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 getPreviewFlags(String name, Predicate<String> hasEntry) {
if (name.startsWith(PACKAGES_PREFIX + "/")) {
throw new IllegalArgumentException(
"Package sub-directory flags handled separately: " + name);
}
// Find suffix for either '/modules/xxx/suffix' or '/xxx/suffix' paths.
int idx = name.startsWith(MODULES_PREFIX + "/") ? MODULES_PREFIX.length() + 1 : 1;
int suffixStart = name.indexOf('/', idx);
if (suffixStart == -1) {
// No flags for '[/modules]/xxx' paths (esp. '/modules', '/packages').
// '/packages/xxx' entries have flags, but not calculated here.
return 0;
}
// Prefix is either '/modules/xxx' or '/xxx', and suffix starts with '/'.
String prefix = name.substring(0, suffixStart);
String suffix = name.substring(suffixStart);
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 {
// Suffix is '/META-INF/xxx' and no preview version is even possible.
return 0;
}
}
/**
* Helper function to calculate package flags for {@code "/packages/xxx"}
* directory entries.
*
* <p>Based on the module links, the flags are:
* <ul>
* <li>{@code FLAGS_HAS_PREVIEW_VERSION} if <em>any</em> referenced
* package has a preview version.
* <li>{@code FLAGS_IS_PREVIEW_ONLY} if <em>all</em> referenced packages
* are preview only.
* </ul>
*
* @return package flags for {@code "/packages/xxx"} directory entries.
*/
public static int getPackageFlags(List<ModuleLink> moduleLinks) {
boolean hasPreviewVersion =
moduleLinks.stream().anyMatch(ModuleLink::hasPreviewVersion);
boolean isPreviewOnly =
moduleLinks.stream().allMatch(ModuleLink::isPreviewOnly);
return (hasPreviewVersion ? ImageLocation.FLAGS_HAS_PREVIEW_VERSION : 0)
| (isPreviewOnly ? ImageLocation.FLAGS_IS_PREVIEW_ONLY : 0);
}
/**
* Tests a non-preview image location's flags to see if it has preview
* content associated with it.
*/
public static boolean hasPreviewVersion(int flags) {
return (flags & FLAGS_HAS_PREVIEW_VERSION) != 0;
}
/**
* Tests an image location's flags to see if it only exists in preview mode.
*/
public static boolean isPreviewOnly(int flags) {
return (flags & FLAGS_IS_PREVIEW_ONLY) != 0;
}
public enum LocationType {
RESOURCE, MODULES_ROOT, MODULES_DIR, PACKAGES_ROOT, PACKAGES_DIR;
}
protected final long[] attributes;
@ -285,6 +444,10 @@ public class ImageLocation {
return (int)getAttribute(ATTRIBUTE_EXTENSION);
}
public int getFlags() {
return (int) getAttribute(ATTRIBUTE_PREVIEW_FLAGS);
}
public String getFullName() {
return getFullName(false);
}
@ -294,7 +457,7 @@ public class ImageLocation {
if (getModuleOffset() != 0) {
if (modulesPrefix) {
builder.append("/modules");
builder.append(MODULES_PREFIX);
}
builder.append('/');
@ -317,36 +480,6 @@ public class ImageLocation {
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 +492,42 @@ public class ImageLocation {
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();
return getBase().charAt(1) == 'p'
? LocationType.PACKAGES_ROOT
: LocationType.MODULES_ROOT;
default:
// Anything else is /<module>/<path> 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);

View File

@ -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,26 @@ import java.nio.file.attribute.BasicFileAttributes;
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.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
@ -77,6 +89,10 @@ import java.util.stream.Stream;
* to the jimage file provided by the shipped JDK by tools running on JDK 8.
*/
public final class ImageReader implements AutoCloseable {
// For resource paths, there's no leading '/'.
private static final String PREVIEW_RESOURCE_PREFIX = PREVIEW_INFIX.substring(1);
private final SharedImageReader reader;
private volatile boolean closed;
@ -86,22 +102,27 @@ public final class ImageReader implements AutoCloseable {
}
/**
* 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.isPreviewModeEnabled());
}
@Override
@ -200,15 +221,45 @@ public final class ImageReader implements AutoCloseable {
return reader.getResourceBuffer(node.getLocation());
}
// Package protected for use only by SystemImageReader.
ResourceEntries getResourceEntries() {
return reader.getResourceEntries();
}
private static final class SharedImageReader extends BasicImageReader {
private static final Map<Path, SharedImageReader> 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 isPreviewEnabled;
public ReaderKey(Path imagePath, boolean isPreviewEnabled) {
this.imagePath = imagePath;
this.isPreviewEnabled = isPreviewEnabled;
}
@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.isPreviewEnabled == other.isPreviewEnabled;
}
return false;
}
@Override
public int hashCode() {
return imagePath.hashCode() ^ Boolean.hashCode(isPreviewEnabled);
}
}
private static final Map<ReaderKey, SharedImageReader> OPEN_FILES = new HashMap<>();
// List of openers for this shared image.
private final Set<ImageReader> openers = new HashSet<>();
@ -219,55 +270,140 @@ public final class ImageReader implements AutoCloseable {
// Cache of all user visible nodes, guarded by synchronizing 'this' instance.
private final Map<String, Node> 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 isPreviewEnabled;
// A relativized mapping from non-preview name to directories containing
// preview-only nodes. This is used to merge preview-only content into
// directories as they are completed.
// E.g. "/modules/xxx/foo/bar" -> Directory("/modules/xxx/META-INF/preview/foo/bar")
private final Map<String, Directory> previewDirectoriesToMerge;
private SharedImageReader(Path imagePath, ByteOrder byteOrder, boolean isPreviewEnabled) 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.isPreviewEnabled = isPreviewEnabled;
// 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 = isPreviewEnabled ? new HashMap<>() : null;
packages.setChildren(processPackagesDirectory(isPreviewEnabled));
}
/**
* Returns the offset of the string denoting the leading "module" segment in
* the given path (e.g. {@code <module>/<path>}). We can't just pass in the
* {@code /<module>} 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}.
*
* <p>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<Node> processPackagesDirectory(boolean previewMode) {
ImageLocation pkgRoot = findLocation(PACKAGES_PREFIX);
assert pkgRoot != null : "Invalid jimage file";
IntBuffer offsets = getOffsetBuffer(pkgRoot);
ArrayList<Node> pkgDirs = new ArrayList<>(offsets.capacity());
// Package path to module map, sorted in reverse order so that
// longer child paths get processed first.
Map<String, List<String>> 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 || !ImageLocation.isPreviewOnly(flags)) {
pkgDirs.add(ensureCached(newDirectory(pkgDir.getFullName())));
}
if (previewMode && ImageLocation.hasPreviewVersion(flags)) {
// Only do this in preview mode for the small set of packages with
// preview versions (the number of preview entries should be small).
List<String> moduleNames = new ArrayList<>();
ModuleLink.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;
}
private static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException {
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<Node> previewOnlyChildren = createChildNodes(previewLoc, 0, childLoc -> {
String baseName = getBaseName(childLoc);
String nonPreviewChildName = nonPreviewDirName + "/" + baseName;
boolean isPreviewOnly = ImageLocation.isPreviewOnly(childLoc.getFlags());
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 (ImageLocation.isPreviewOnly(previewLoc.getFlags())) {
// 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);
}
}
// Adds a node to the cache, ensuring that no matching entry already existed.
private <T extends Node> T ensureCached(T node) {
Node existingNode = nodes.put(node.getName(), node);
assert existingNode == null : "Unexpected node already cached for: " + node;
return node;
}
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");
}
@ -291,7 +427,7 @@ public final class ImageReader implements AutoCloseable {
close();
nodes.clear();
if (!OPEN_FILES.remove(this.getImagePath(), this)) {
if (!OPEN_FILES.remove(new ReaderKey(getImagePath(), isPreviewEnabled), this)) {
throw new IOException("image file not found in open list");
}
}
@ -309,20 +445,14 @@ public final class ImageReader implements AutoCloseable {
* "/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.
@ -341,18 +471,27 @@ public final class ImageReader implements AutoCloseable {
* the node handling code.
*/
Node findResourceNode(String moduleName, String resourcePath) {
// Unlike findNode(), this method makes only one lookup in the
// underlying jimage, but can only reliably return resource nodes.
// Unlike findNode(), this method can only reliably return resource nodes.
if (moduleName.indexOf('/') >= 0) {
throw new IllegalArgumentException("invalid module name: " + moduleName);
}
String nodeName = MODULES_ROOT + "/" + moduleName + "/" + resourcePath;
if (resourcePath.startsWith(PREVIEW_RESOURCE_PREFIX)) {
return null;
}
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)) {
ImageLocation loc = null;
if (isPreviewEnabled) {
// We must test preview location first (if in preview mode).
loc = findLocation(moduleName, PREVIEW_RESOURCE_PREFIX + resourcePath);
}
if (loc == null) {
loc = findLocation(moduleName, resourcePath);
}
if (loc != null && loc.getType() == RESOURCE) {
node = newResource(nodeName, loc);
nodes.put(node.getName(), node);
}
@ -368,18 +507,36 @@ public final class ImageReader implements AutoCloseable {
*
* <p>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.
ImageLocation loc = findLocation(moduleName, resourcePath);
return loc != null && isResource(loc);
if (resourcePath.startsWith(PREVIEW_RESOURCE_PREFIX)) {
return false;
}
// In preview mode, preview-only resources are eagerly added to the
// cache, so we must check that first.
ImageLocation loc = null;
if (isPreviewEnabled) {
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();
}
}
loc = findLocation(moduleName, PREVIEW_RESOURCE_PREFIX + resourcePath);
}
if (loc == null) {
loc = findLocation(moduleName, resourcePath);
}
return loc != null && loc.getType() == RESOURCE;
}
/**
@ -388,55 +545,82 @@ public final class ImageReader implements AutoCloseable {
* <p>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 loc != null && loc.getType() == RESOURCE
? ensureCached(newResource(name, loc))
: null;
}
/**
* Builds a node in the "/packages/..." namespace.
*
* <p>Called by {@link #findNode(String)} if a {@code /packages/...} node
* is not present in the cache.
* Returns whether a directory name in the "/modules/" directory could be referencing
* the "META-INF" directory".
*/
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 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".
*
* <p>Called by {@link #findNode(String)} if a {@code /packages/...}
* node is not present in the cache (the name is not trusted).
*/
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;
@ -448,127 +632,125 @@ public final class ImageReader implements AutoCloseable {
// 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.
*
* <p>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<Node> children = createChildNodes(loc, childLoc -> {
if (isModulesSubdirectory(childLoc)) {
return nodes.computeIfAbsent(childLoc.getFullName(), this::newDirectory);
List<Node> 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<Node> 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.
*
* <p>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/<package>". However, unlike "/modules" directories, the
// location offsets mean different things.
List<Node> 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<Node> children = new ArrayList<>(offsets.capacity() / 2);
ModuleLink.readNameOffsets(offsets, /*normal*/ true, isPreviewEnabled)
.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.
*
* <p>Because this is only called once per-directory (since the result is cached
* indefinitely) we can remove any entries we find from the cache. If ever the
* node cache allowed entries to expire, this would have to be changed so that
* directories could be completed more than once.
*/
List<Node> getPreviewNodesToMerge(Directory dir) {
if (previewDirectoriesToMerge != null) {
Directory mergeDir = previewDirectoriesToMerge.remove(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.
*
* <p>Note: This cannot be used for package subdirectories as they have
* child offsets stored differently to other directories.
* <p>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.
*
* @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<Node> createChildNodes(ImageLocation loc, Function<ImageLocation, Node> newChildFn) {
private List<Node> createChildNodes(ImageLocation loc, int extraNodesCount, Function<ImageLocation, Node> 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<Node> children = new ArrayList<>(childCount);
List<Node> 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.
*
* <p>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).
*
* <p>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
@ -584,7 +766,6 @@ public final class ImageReader implements AutoCloseable {
* 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);
}
@ -816,7 +997,7 @@ public final class ImageReader implements AutoCloseable {
throw new IllegalStateException("Cannot get child nodes of an incomplete directory: " + getName());
}
private void setChildren(List<Node> children) {
private void setChildren(List<? extends Node> children) {
assert this.children == null : this + ": Cannot set child nodes twice!";
this.children = Collections.unmodifiableList(children);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2026, 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,23 @@ package jdk.internal.jimage;
* 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.
// They are validated during initialization of ImageStringsWriter.
//
// Adding new strings (with larger offsets) is possible without changing
// the jimage version number, but any change to existing strings must be
// accompanied by a jimage version number change.
/** 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);

View File

@ -0,0 +1,290 @@
/*
* Copyright (c) 2025, 2026, 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.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Function;
/**
* Represents links to modules stored in the buffer of {@code "/packages/xxx"}
* image locations (package subdirectories).
*
* <p>Package subdirectories store their data differently to all other jimage
* entries. Instead of storing a sequence of offsets to their child entries,
* they store a flattened representation of the child's data in an interleaved
* buffer. These entries also use flags which are similar to, but distinct from,
* the {@link ImageLocation} flags.
*
* <p>This class encapsulates that complexity to help 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 ModuleLink implements Comparable<ModuleLink> {
// These flags are additive (hence "has-content" rather than "is-empty").
/** If set, this package exists in preview mode. */
private static final int FLAGS_PKG_HAS_PREVIEW_VERSION = 0x1;
/** If set, this package exists in non-preview mode. */
private static final int FLAGS_PKG_HAS_NORMAL_VERSION = 0x2;
/** If set, the associated module has resources (in normal or preview mode). */
private static final int FLAGS_PKG_HAS_RESOURCES = 0x4;
/**
* Links 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<ModuleLink> PREVIEW_FIRST =
Comparator.comparing(ModuleLink::hasPreviewVersion).reversed()
.thenComparing(ModuleLink::name);
/**
* Returns a link for non-empty packages (those with resources) in a
* given module.
*
* <p>The same link can be used for multiple packages in the same module.
*
* @param moduleName the name of the module in which this package exits.
* @param isPreview whether the associated package is defined for preview mode.
*/
public static ModuleLink forPackage(String moduleName, boolean isPreview) {
return new ModuleLink(moduleName, FLAGS_PKG_HAS_RESOURCES | previewFlag(isPreview));
}
/**
* Returns a link for empty packages in a given module.
*
* <p>The same link can be used for multiple packages in the same module.
*
* @param moduleName the name of the module in which this package exits.
* @param isPreview whether the associated package is defined for preview mode.
*/
public static ModuleLink forEmptyPackage(String moduleName, boolean isPreview) {
return new ModuleLink(moduleName, previewFlag(isPreview));
}
/**
* Returns the appropriate FLAGS_PKG_HAS_XXX_VERSION constant according to
* whether the associated package is defined for preview mode.
*/
private static int previewFlag(boolean isPreview) {
return isPreview ? FLAGS_PKG_HAS_PREVIEW_VERSION : FLAGS_PKG_HAS_NORMAL_VERSION;
}
/** Merges two links for the same module (combining their flags). */
public ModuleLink merge(ModuleLink 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 ModuleLink(name, flags | other.flags);
}
private final String name;
private final int flags;
private ModuleLink(String moduleName, int flags) {
this.name = Objects.requireNonNull(moduleName);
this.flags = flags;
}
/** Returns the module name of this link. */
public String name() {
return name;
}
/**
* Returns whether the package associated with this link contains resources
* in its module.
*
* <p>An invariant of the module system is that while a package may exist
* under many modules, it only has resources in one.
*/
public boolean hasResources() {
return (flags & FLAGS_PKG_HAS_RESOURCES) != 0;
}
/**
* Returns whether the package associated with this module link has a
* preview version (empty or otherwise) in this link's module.
*/
public boolean hasPreviewVersion() {
return (flags & FLAGS_PKG_HAS_PREVIEW_VERSION) != 0;
}
/** Returns whether this module link exists only in preview mode. */
public boolean isPreviewOnly() {
return (flags & FLAGS_PKG_HAS_NORMAL_VERSION) == 0;
}
@Override
public int compareTo(ModuleLink rhs) {
return PREVIEW_FIRST.compare(this, rhs);
}
@Override
public String toString() {
return "ModuleLink{ module=" + name + ", flags=" + flags + " }";
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ModuleLink)) {
return false;
}
ModuleLink other = (ModuleLink) 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.
*
* <p>Package subdirectories store their entries using pairs of integers in
* an interleaved buffer:
* <pre>
* ...
* [ entry-N flags ]
* [ entry-N name offset ]
* [ entry-(N+1) flags ]
* [ entry-(N+1) name offset ]
* ...
* </pre>
*
* <p>Entry flags control whether an entry name should be included by the
* returned iterator, depending on the given include-flags.
*
* @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<Integer> readNameOffsets(
IntBuffer buffer, boolean includeNormal, boolean includePreview) {
int bufferSize = buffer.capacity();
if (bufferSize == 0 || (bufferSize & 0x1) != 0) {
throw new IllegalArgumentException("Invalid buffer size");
}
int includeMask = (includeNormal ? FLAGS_PKG_HAS_NORMAL_VERSION : 0)
+ (includePreview ? FLAGS_PKG_HAS_PREVIEW_VERSION : 0);
if (includeMask == 0) {
throw new IllegalArgumentException("Invalid flags");
}
return new Iterator<Integer>() {
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.
if ((buffer.get(idx) & includeMask) != 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 links to a given buffer. The given entry list is
* checked carefully to ensure the written buffer will be valid.
*
* <p>Entries are written in order, taking two integer slots per entry as
* {@code [<flags>, <encoded-name>]}.
*
* @param links the module links to write, correctly ordered.
* @param buffer destination buffer.
* @param nameEncoder encoder for module names.
* @throws IllegalArgumentException in the link entries are invalid in any way.
*/
public static void write(
List<ModuleLink> links, IntBuffer buffer, Function<String, Integer> nameEncoder) {
if (links.isEmpty()) {
throw new IllegalArgumentException("References list must be non-empty");
}
int expectedCapacity = 2 * links.size();
if (buffer.capacity() != expectedCapacity) {
throw new IllegalArgumentException(
"Invalid buffer capacity: expected " + expectedCapacity + ", got " + buffer.capacity());
}
// This catches exact duplicates in the list.
links.stream().reduce((lhs, rhs) -> {
if (lhs.compareTo(rhs) >= 0) {
throw new IllegalArgumentException("References must be strictly ordered: " + links);
}
return rhs;
});
// Distinct references can have the same name (but we don't allow this).
if (links.stream().map(ModuleLink::name).distinct().count() != links.size()) {
throw new IllegalArgumentException("Module links names must be unique: " + links);
}
if (links.stream().filter(ModuleLink::hasResources).count() > 1) {
throw new IllegalArgumentException("At most one module link can have resources: " + links);
}
for (ModuleLink link : links) {
buffer.put(link.flags);
buffer.put(nameEncoder.apply(link.name));
}
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright (c) 2025, 2026, 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,
/**
* Preview mode is enabled. If preview classes or resources exist in the jimage file,
* they will be made available.
*/
ENABLED,
/**
* The preview mode of the current run-time, typically determined by the
* {@code --enable-preview} flag.
*/
FOR_RUNTIME;
/**
* Resolves whether preview mode should be enabled for an {@link ImageReader}.
*/
public boolean isPreviewModeEnabled() {
// A switch, instead of an abstract method, saves 3 subclasses.
switch (this) {
case DISABLED:
return false;
case ENABLED:
return true;
case FOR_RUNTIME:
// 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 InternalError(e);
}
default:
throw new IllegalStateException("Invalid mode: " + this);
}
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) 2025, 2026, 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.util.stream.Stream;
/**
* Accesses the underlying resource entries in a jimage file.
*
* <p>This is a special-case API designed only for use by the jlink tool,
* which read the raw jimage files. It is not the correct API for accessing
* jimage resources at runtime.
*
* <p>This API ignores the {@code previewMode} of the {@link ImageReader} from
* which it is obtained, and returns an unmapped view of entries (e.g. allowing
* for direct access of resources in the {@code META-INF/preview/...} namespace).
*
* <p>It disallows access to resource directories (i.e. {@code "/modules/..."})
* or packages entries (i.e. {@code "/packages/..."}).
*
* <p>Use the {@link ImageReader} API to correctly account for preview mode at
* runtime.
*
* @implNote This class needs to maintain JDK 8 source compatibility.
*
* It is used internally in the JDK to implement jimage/jrtfs access,
* but also compiled and delivered as part of the jrtfs.jar to support access
* to the jimage file provided by the shipped JDK by tools running on JDK 8.
*/
public interface ResourceEntries {
/**
* Returns the jimage names for all resources in the given module, in
* random order. Entry names will always be prefixed by the given module
* name (e.g. {@code "/<module-name>/..."}).
*/
Stream<String> getEntryNames(String module);
/**
* Returns the (uncompressed) size of a resource given its jimage name.
*
* @throws java.util.NoSuchElementException if the resource does not exist.
*/
long getSize(String name);
/**
* Returns a copy of a resource's content given its jimage name.
*
* @throws java.util.NoSuchElementException if the resource does not exist.
*/
byte[] getBytes(String name);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2026, 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.IOException;
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 @@ import java.util.function.Function;
* 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,32 @@ public class ImageReaderFactory {
} else {
fs = FileSystems.getDefault();
}
BOOT_MODULES_JIMAGE = fs.getPath(JAVA_HOME, "lib", "modules");
}
private static final Map<Path, ImageReader> 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<Path, ImageReader> OPENER = new Function<Path, ImageReader>() {
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;
}
/**
* Returns the "raw" API for accessing underlying jimage resource entries.
*
* <p>This is only meaningful for use by code dealing directly with jimage
* files, and cannot be used to reliably lookup resources used at runtime.
*/
public static ResourceEntries getResourceEntries() {
return get().getResourceEntries();
}
private SystemImageReader() {}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2026, 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
@ -63,6 +63,7 @@ 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.
@ -85,7 +86,8 @@ class JrtFileSystem extends FileSystem {
throws IOException
{
this.provider = provider;
this.image = SystemImage.open(); // open image file
// TODO: Obtain and pass correct preview mode flag value here.
this.image = SystemImage.open(PreviewMode.DISABLED); // open image file
this.isOpen = true;
this.isClosable = env != null;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2026, 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
@ -39,6 +39,7 @@ import java.security.PrivilegedAction;
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.
@ -47,35 +48,49 @@ import jdk.internal.jimage.ImageReader.Node;
* 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.
*/
@SuppressWarnings({ "removal", "suppression"} )
abstract class SystemImage {
@SuppressWarnings({"removal", "suppression"})
public abstract class SystemImage implements AutoCloseable {
abstract Node findNode(String path) throws IOException;
abstract byte[] getResource(Node node) throws IOException;
abstract void close() throws IOException;
public abstract Node findNode(String path) throws IOException;
public abstract byte[] getResource(Node node) throws IOException;
public abstract void close() throws IOException;
static SystemImage open() throws IOException {
if (modulesImageExists) {
// open a .jimage and build directory structure
final ImageReader image = ImageReader.open(moduleImageFile);
return new SystemImage() {
@Override
Node findNode(String path) throws IOException {
return image.findNode(path);
}
@Override
byte[] getResource(Node node) throws IOException {
return image.getResource(node);
}
@Override
void close() throws IOException {
image.close();
}
};
/**
* Opens the system image for the current runtime.
*
* @param mode determines whether preview mode should be enabled.
* @return a new system image based on either the jimage file or an "exploded"
* modules directory, according to the build state.
*/
public static SystemImage open(PreviewMode mode) throws IOException {
return modulesImageExists ? fromJimage(moduleImageFile, mode) : fromDirectory(explodedModulesDir, mode);
}
/** Internal factory method for testing only, use {@link SystemImage#open(PreviewMode)}. */
public static SystemImage fromJimage(Path path, PreviewMode mode) throws IOException {
final ImageReader image = ImageReader.open(path, mode);
return new SystemImage() {
@Override
public Node findNode(String path) throws IOException {
return image.findNode(path);
}
@Override
public byte[] getResource(Node node) throws IOException {
return image.getResource(node);
}
@Override
public void close() throws IOException {
image.close();
}
};
}
/** Internal factory method for testing only, use {@link SystemImage#open(PreviewMode)}. */
public static SystemImage fromDirectory(Path modulesDir, PreviewMode mode) throws IOException {
if (!Files.isDirectory(modulesDir)) {
throw new FileSystemNotFoundException(modulesDir.toString());
}
if (Files.notExists(explodedModulesDir))
throw new FileSystemNotFoundException(explodedModulesDir.toString());
return new ExplodedImage(explodedModulesDir);
return new ExplodedImage(modulesDir);
}
private static final String RUNTIME_HOME;

View File

@ -54,7 +54,7 @@ import java.util.stream.Stream;
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 final class SystemModuleFinders {
* 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;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2026, 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
@ -34,7 +34,7 @@ import java.net.URL;
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 @@ import sun.net.www.URLConnection;
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,10 @@ public class JavaRuntimeURLConnection extends URLConnection {
@Override
public long getContentLengthLong() {
// connectResourceNode() may throw UncheckedIOException.
try {
return connectResourceNode().size();
} catch (IOException ioe) {
} catch (IOException | UncheckedIOException ioe) {
return -1L;
}
}
@ -124,6 +125,10 @@ public class JavaRuntimeURLConnection extends URLConnection {
// 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 (common case).
return path;
}
// Any additional special case decoding logic should go here.
try {
return ParseUtil.decode(path);

View File

@ -357,8 +357,8 @@ u4 ImageFileReader::find_location_index(const char* path, u8 *size) const {
ImageLocation location(data);
// Make sure result is not a false positive.
if (verify_location(location, path)) {
*size = (jlong)location.get_attribute(ImageLocation::ATTRIBUTE_UNCOMPRESSED);
return offset;
*size = (jlong)location.get_attribute(ImageLocation::ATTRIBUTE_UNCOMPRESSED);
return offset;
}
}
return 0; // not found
@ -389,7 +389,7 @@ bool ImageFileReader::verify_location(ImageLocation& location, const char* path)
}
// Get base name string.
const char* base = location.get_attribute(ImageLocation::ATTRIBUTE_BASE, strings);
// Compare with basne name.
// Compare with base name.
if (!(next = ImageStrings::starts_with(next, base))) return false;
// Get extension string.
const char* extension = location.get_attribute(ImageLocation::ATTRIBUTE_EXTENSION, strings);

View File

@ -232,18 +232,34 @@ public:
//
class ImageLocation {
public:
// See also src/java.base/share/classes/jdk/internal/jimage/ImageLocation.java
enum {
ATTRIBUTE_END, // End of attribute stream marker
ATTRIBUTE_MODULE, // String table offset of module name
ATTRIBUTE_PARENT, // String table offset of resource path parent
ATTRIBUTE_BASE, // String table offset of resource path base
ATTRIBUTE_EXTENSION, // String table offset of resource path extension
ATTRIBUTE_EXTENSION, // String table offset of resource path extension
ATTRIBUTE_OFFSET, // Container byte offset of resource
ATTRIBUTE_COMPRESSED, // In image byte size of the compressed resource
ATTRIBUTE_UNCOMPRESSED, // In memory byte size of the uncompressed resource
ATTRIBUTE_COMPRESSED, // In-image byte size of the compressed resource
ATTRIBUTE_UNCOMPRESSED, // In-memory byte size of the uncompressed resource
ATTRIBUTE_PREVIEW_FLAGS, // Flags relating to preview mode resources.
ATTRIBUTE_COUNT // Number of attribute kinds
};
// Flag masks for the ATTRIBUTE_PREVIEW_FLAGS attribute. Defined so
// that zero is the overwhelmingly common case for normal resources.
// See also src/java.base/share/classes/jdk/internal/jimage/ImageLocation.java
enum {
// Set on a "normal" (non-preview) location if a preview version of
// it exists in the same module.
FLAGS_HAS_PREVIEW_VERSION = 0x1,
// Set on all preview locations in "/modules/xxx/META-INF/preview/..."
FLAGS_IS_PREVIEW_VERSION = 0x2,
// Set on a preview location if no normal (non-preview) version of
// it exists in the same module.
FLAGS_IS_PREVIEW_ONLY = 0x4
};
private:
// Values of inflated attributes.
u8 _attributes[ATTRIBUTE_COUNT];
@ -300,6 +316,11 @@ public:
inline const char* get_attribute(u4 kind, const ImageStrings& strings) const {
return strings.get((u4)get_attribute(kind));
}
// Retrieve flags from the ATTRIBUTE_PREVIEW_FLAGS attribute.
inline u4 get_preview_flags() const {
return (u4) get_attribute(ATTRIBUTE_PREVIEW_FLAGS);
}
};
// Image file header, starting at offset 0.
@ -391,6 +412,7 @@ public:
// leads the ImageFileReader to be actually closed and discarded.
class ImageFileReader {
friend class ImageFileReaderTable;
friend class PackageFlags;
private:
// Manage a number of image files such that an image can be shared across
// multiple uses (ex. loader.)
@ -430,7 +452,7 @@ public:
// Image file major version number.
MAJOR_VERSION = 1,
// Image file minor version number.
MINOR_VERSION = 0
MINOR_VERSION = 1
};
// Locate an image if file already open.

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2026, Oracle and/or its affiliates. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@ -91,45 +91,106 @@ JIMAGE_Close(JImageFile* image) {
* name, a version string and the name of a class/resource, return location
* information describing the resource and its size. If no resource is found, the
* function returns JIMAGE_NOT_FOUND and the value of size is undefined.
* The version number should be "9.0" and is not used in locating the resource.
* The resulting location does/should not have to be released.
* All strings are utf-8, zero byte terminated.
*
* Ex.
* jlong size;
* JImageLocationRef location = (*JImageFindResource)(image,
* "java.base", "9.0", "java/lang/String.class", &size);
* "java.base", "java/lang/String.class", is_preview_mode, &size);
*/
extern "C" JNIEXPORT JImageLocationRef
JIMAGE_FindResource(JImageFile* image,
const char* module_name, const char* version, const char* name,
const char* module_name, const char* name, bool is_preview_mode,
jlong* size) {
// Concatenate to get full path
char fullpath[IMAGE_MAX_PATH];
size_t moduleNameLen = strlen(module_name);
size_t nameLen = strlen(name);
size_t index;
static const char str_modules[] = "modules";
static const char str_packages[] = "packages";
static const char preview_infix[] = "/META-INF/preview";
assert(moduleNameLen > 0 && "module name must be non-empty");
assert(nameLen > 0 && "name must non-empty");
size_t module_name_len = strlen(module_name);
size_t name_len = strlen(name);
size_t preview_infix_len = strlen(preview_infix);
assert(module_name_len > 0 && "module name must be non-empty");
assert(name_len > 0 && "name must non-empty");
// If the concatenated string is too long for the buffer, return not found
if (1 + moduleNameLen + 1 + nameLen + 1 > IMAGE_MAX_PATH) {
// Do not attempt to lookup anything of the form /modules/... or /packages/...
if (strncmp(module_name, str_modules, sizeof(str_modules)) == 0
|| strncmp(module_name, str_packages, sizeof(str_packages)) == 0) {
return 0L;
}
// If the preview mode version of the path string is too long for the buffer,
// return not found (even when not in preview mode).
if (1 + module_name_len + preview_infix_len + 1 + name_len + 1 > IMAGE_MAX_PATH) {
return 0L;
}
index = 0;
fullpath[index++] = '/';
memcpy(&fullpath[index], module_name, moduleNameLen);
index += moduleNameLen;
fullpath[index++] = '/';
memcpy(&fullpath[index], name, nameLen);
index += nameLen;
fullpath[index++] = '\0';
// Concatenate to get full path
char name_buffer[IMAGE_MAX_PATH];
char* path;
{ // Write the buffer with room to prepend the preview mode infix
// at the start (saves copying the trailing name part twice).
size_t index = preview_infix_len;
name_buffer[index++] = '/';
memcpy(&name_buffer[index], module_name, module_name_len);
index += module_name_len;
name_buffer[index++] = '/';
memcpy(&name_buffer[index], name, name_len);
index += name_len;
name_buffer[index++] = '\0';
// Path begins at the leading '/' (not the start of the buffer).
path = &name_buffer[preview_infix_len];
}
JImageLocationRef loc =
(JImageLocationRef) ((ImageFileReader*) image)->find_location_index(fullpath, (u8*) size);
return loc;
// find_location_index() returns the data "offset", not an index.
const ImageFileReader* image_file = (ImageFileReader*) image;
u4 locOffset = image_file->find_location_index(path, (u8*) size);
if (locOffset != 0) {
ImageLocation loc;
loc.set_data(image_file->get_location_offset_data(locOffset));
u4 flags = loc.get_preview_flags();
// No preview flags means "a normal resource, without a preview version".
// This is the overwhelmingly common case, with or without preview mode.
if (flags == 0) {
return locOffset;
}
// Regardless of preview mode, don't return resources requested directly
// via their preview path.
if ((flags & ImageLocation::FLAGS_IS_PREVIEW_VERSION) != 0) {
return 0L;
}
// Even if there is a preview version, we might not want to return it.
if (!is_preview_mode || (flags & ImageLocation::FLAGS_HAS_PREVIEW_VERSION) == 0) {
return locOffset;
}
} else if (!is_preview_mode) {
// No normal resource found, and not in preview mode.
return 0L;
}
// We are in preview mode, and the preview version of the resource is needed.
// This is either because:
// 1. The normal resource was flagged as having a preview version (rare)
// 2. This is a preview-only resource (there was no normal resource, very rare)
// 3. The requested resource doesn't exist (this should typically not happen)
//
// Since we only expect requests for resources which exist in jimage files, we
// expect this 2nd lookup to succeed (this is contrary to the expectations for
// the JRT file system, where non-existent resource lookups are common).
{ // Rewrite the front of the name buffer to make it a preview path.
size_t index = 0;
name_buffer[index++] = '/';
memcpy(&name_buffer[index], module_name, module_name_len);
index += module_name_len;
memcpy(&name_buffer[index], preview_infix, preview_infix_len);
index += preview_infix_len;
// Check we copied up to the expected '/' separator.
assert(name_buffer[index] == '/' && "bad string concatenation");
// The preview path now begins at the start of the buffer.
path = &name_buffer[0];
}
return image_file->find_location_index(path, (u8*) size);
}
/*

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2026, Oracle and/or its affiliates. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@ -98,21 +98,20 @@ typedef void (*JImageClose_t)(JImageFile* jimage);
* name, a version string and the name of a class/resource, return location
* information describing the resource and its size. If no resource is found, the
* function returns JIMAGE_NOT_FOUND and the value of size is undefined.
* The version number should be "9.0" and is not used in locating the resource.
* The resulting location does/should not have to be released.
* All strings are utf-8, zero byte terminated.
*
* Ex.
* jlong size;
* JImageLocationRef location = (*JImageFindResource)(image,
* "java.base", "9.0", "java/lang/String.class", &size);
* "java.base", "java/lang/String.class", is_preview_mode, &size);
*/
extern "C" JNIEXPORT JImageLocationRef JIMAGE_FindResource(JImageFile* jimage,
const char* module_name, const char* version, const char* name,
const char* module_name, const char* name, bool is_preview_mode,
jlong* size);
typedef JImageLocationRef(*JImageFindResource_t)(JImageFile* jimage,
const char* module_name, const char* version, const char* name,
const char* module_name, const char* name, bool is_preview_mode,
jlong* size);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2026, 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
@ -298,7 +298,7 @@ class JImageTask {
parent.getAbsolutePath());
}
if (!ImageResourcesTree.isTreeInfoResource(name)) {
if (location.getType() == ImageLocation.LocationType.RESOURCE) {
Files.write(resource.toPath(), bytes);
}
}
@ -415,21 +415,18 @@ class JImageTask {
continue;
}
if (!ImageResourcesTree.isTreeInfoResource(name)) {
ImageLocation location = reader.findLocation(name);
if (location.getType() == ImageLocation.LocationType.RESOURCE) {
if (moduleAction != null) {
int offset = name.indexOf('/', 1);
String newModule = offset != -1 ?
name.substring(1, offset) :
"<unknown>";
String newModule = location.getModule();
if (newModule.isEmpty()) {
newModule = "<unknown>";
}
if (!oldModule.equals(newModule)) {
moduleAction.apply(reader, oldModule, newModule);
oldModule = newModule;
}
}
ImageLocation location = reader.findLocation(name);
resourceAction.apply(reader, name, location);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2026, 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
@ -24,6 +24,7 @@
*/
package jdk.tools.jlink.internal;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
@ -34,7 +35,7 @@ import java.util.stream.Stream;
* An Archive of all content, classes, resources, configuration files, and
* other, for a module.
*/
public interface Archive {
public interface Archive extends Closeable {
/**
* Entry is contained in an Archive
@ -59,11 +60,12 @@ public interface Archive {
private final String path;
/**
* Constructs an entry of the given archive
* @param archive archive
* @param path
* @param name an entry name that does not contain the module name
* @param type
* Constructs an entry of the given archive.
*
* @param archive the archive in which this entry exists.
* @param path the complete path of the entry, including the module.
* @param name an entry name relative to its containing module.
* @param type the entry type.
*/
public Entry(Archive archive, String path, String name, EntryType type) {
this.archive = Objects.requireNonNull(archive);
@ -72,10 +74,6 @@ public interface Archive {
this.type = Objects.requireNonNull(type);
}
public final Archive archive() {
return archive;
}
public final EntryType type() {
return type;
}
@ -134,5 +132,6 @@ public interface Archive {
/*
* Close the archive
*/
@Override
void close() throws IOException;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2026, 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
@ -36,21 +36,17 @@ import jdk.internal.jimage.ImageStringsReader;
public final class BasicImageWriter {
public static final String MODULES_IMAGE_NAME = "modules";
private ByteOrder byteOrder;
private ImageStringsWriter strings;
private final ByteOrder byteOrder;
private final ImageStringsWriter strings;
private int length;
private int[] redirect;
private ImageLocationWriter[] locations;
private List<ImageLocationWriter> input;
private ImageStream headerStream;
private ImageStream redirectStream;
private ImageStream locationOffsetStream;
private ImageStream locationStream;
private ImageStream allIndexStream;
public BasicImageWriter() {
this(ByteOrder.nativeOrder());
}
private final List<ImageLocationWriter> input;
private final ImageStream headerStream;
private final ImageStream redirectStream;
private final ImageStream locationOffsetStream;
private final ImageStream locationStream;
private final ImageStream allIndexStream;
public BasicImageWriter(ByteOrder byteOrder) {
this.byteOrder = Objects.requireNonNull(byteOrder);
@ -75,11 +71,15 @@ public final class BasicImageWriter {
return strings.get(offset);
}
public void addLocation(String fullname, long contentOffset,
long compressedSize, long uncompressedSize) {
public void addLocation(
String fullname,
long contentOffset,
long compressedSize,
long uncompressedSize,
int previewFlags) {
ImageLocationWriter location =
ImageLocationWriter.newLocation(fullname, strings,
contentOffset, compressedSize, uncompressedSize);
contentOffset, compressedSize, uncompressedSize, previewFlags);
input.add(location);
length++;
}
@ -88,10 +88,6 @@ public final class BasicImageWriter {
return locations;
}
int getLocationsCount() {
return input.size();
}
private void generatePerfectHash() {
PerfectHashBuilder<ImageLocationWriter> builder =
new PerfectHashBuilder<>(
@ -174,16 +170,4 @@ public final class BasicImageWriter {
return allIndexStream.toArray();
}
ImageLocationWriter find(String key) {
int index = redirect[ImageStringsReader.hashCode(key) % length];
if (index < 0) {
index = -index - 1;
} else {
index = ImageStringsReader.hashCode(key, index) % length;
}
return locations[index];
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2026, 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
@ -49,6 +49,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.internal.jimage.ImageLocation;
import jdk.tools.jlink.internal.Archive.Entry;
import jdk.tools.jlink.internal.Archive.Entry.EntryType;
import jdk.tools.jlink.internal.JRTArchive.ResourceFileEntry;
@ -227,31 +228,8 @@ public final class ImageFileCreator {
DataOutputStream out,
boolean generateRuntimeImage
) throws IOException {
ResourcePool resultResources;
try {
resultResources = pluginSupport.visitResources(allContent);
if (generateRuntimeImage) {
// Keep track of non-modules resources for linking from a run-time image
resultResources = addNonClassResourcesTrackFiles(resultResources,
writer);
// Generate the diff between the input resources from packaged
// modules in 'allContent' to the plugin- or otherwise
// generated-content in 'resultResources'
resultResources = addResourceDiffFiles(allContent.resourcePool(),
resultResources,
writer);
}
} catch (PluginException pe) {
if (JlinkTask.DEBUG) {
pe.printStackTrace();
}
throw pe;
} catch (Exception ex) {
if (JlinkTask.DEBUG) {
ex.printStackTrace();
}
throw new IOException(ex);
}
ResourcePool resultResources =
getResourcePool(allContent, writer, pluginSupport, generateRuntimeImage);
Set<String> duplicates = new HashSet<>();
long[] offset = new long[1];
@ -282,8 +260,10 @@ public final class ImageFileCreator {
offset[0] += onFileSize;
return;
}
int locFlags = ImageLocation.getPreviewFlags(
res.path(), p -> resultResources.findEntry(p).isPresent());
duplicates.add(path);
writer.addLocation(path, offset[0], compressedSize, uncompressedSize);
writer.addLocation(path, offset[0], compressedSize, uncompressedSize, locFlags);
paths.add(path);
offset[0] += onFileSize;
}
@ -307,6 +287,40 @@ public final class ImageFileCreator {
return resultResources;
}
private static ResourcePool getResourcePool(
ResourcePoolManager allContent,
BasicImageWriter writer,
ImagePluginStack pluginSupport,
boolean generateRuntimeImage)
throws IOException {
ResourcePool resultResources;
try {
resultResources = pluginSupport.visitResources(allContent);
if (generateRuntimeImage) {
// Keep track of non-modules resources for linking from a run-time image
resultResources = addNonClassResourcesTrackFiles(resultResources,
writer);
// Generate the diff between the input resources from packaged
// modules in 'allContent' to the plugin- or otherwise
// generated-content in 'resultResources'
resultResources = addResourceDiffFiles(allContent.resourcePool(),
resultResources,
writer);
}
} catch (PluginException pe) {
if (JlinkTask.DEBUG) {
pe.printStackTrace();
}
throw pe;
} catch (Exception ex) {
if (JlinkTask.DEBUG) {
ex.printStackTrace();
}
throw new IOException(ex);
}
return resultResources;
}
/**
* Support for creating a runtime suitable for linking from the run-time
* image.
@ -558,62 +572,4 @@ public final class ImageFileCreator {
resultResources.entries().forEach(resources::add);
return resources;
}
/**
* Helper method that splits a Resource path onto 3 items: module, parent
* and resource name.
*
* @param path
* @return An array containing module, parent and name.
*/
public static String[] splitPath(String path) {
Objects.requireNonNull(path);
String noRoot = path.substring(1);
int pkgStart = noRoot.indexOf("/");
String module = noRoot.substring(0, pkgStart);
List<String> result = new ArrayList<>();
result.add(module);
String pkg = noRoot.substring(pkgStart + 1);
String resName;
int pkgEnd = pkg.lastIndexOf("/");
if (pkgEnd == -1) { // No package.
resName = pkg;
} else {
resName = pkg.substring(pkgEnd + 1);
}
pkg = toPackage(pkg, false);
result.add(pkg);
result.add(resName);
String[] array = new String[result.size()];
return result.toArray(array);
}
/**
* Returns the path of the resource.
*/
public static String resourceName(String path) {
Objects.requireNonNull(path);
String s = path.substring(1);
int index = s.indexOf("/");
return s.substring(index + 1);
}
public static String toPackage(String name) {
return toPackage(name, false);
}
private static String toPackage(String name, boolean log) {
int index = name.lastIndexOf('/');
if (index > 0) {
return name.substring(0, index).replace('/', '.');
} else {
// ## unnamed package
if (log) {
System.err.format("Warning: %s in unnamed package%n", name);
}
return "";
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2026, 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
@ -53,9 +53,13 @@ public final class ImageLocationWriter extends ImageLocation {
return addAttribute(kind, strings.add(value));
}
static ImageLocationWriter newLocation(String fullName,
static ImageLocationWriter newLocation(
String fullName,
ImageStringsWriter strings,
long contentOffset, long compressedSize, long uncompressedSize) {
long contentOffset,
long compressedSize,
long uncompressedSize,
int previewFlags) {
String moduleName = "";
String parentName = "";
String baseName;
@ -90,13 +94,14 @@ public final class ImageLocationWriter extends ImageLocation {
}
return new ImageLocationWriter(strings)
.addAttribute(ATTRIBUTE_MODULE, moduleName)
.addAttribute(ATTRIBUTE_PARENT, parentName)
.addAttribute(ATTRIBUTE_BASE, baseName)
.addAttribute(ATTRIBUTE_EXTENSION, extensionName)
.addAttribute(ATTRIBUTE_OFFSET, contentOffset)
.addAttribute(ATTRIBUTE_COMPRESSED, compressedSize)
.addAttribute(ATTRIBUTE_UNCOMPRESSED, uncompressedSize);
.addAttribute(ATTRIBUTE_MODULE, moduleName)
.addAttribute(ATTRIBUTE_PARENT, parentName)
.addAttribute(ATTRIBUTE_BASE, baseName)
.addAttribute(ATTRIBUTE_EXTENSION, extensionName)
.addAttribute(ATTRIBUTE_OFFSET, contentOffset)
.addAttribute(ATTRIBUTE_COMPRESSED, compressedSize)
.addAttribute(ATTRIBUTE_UNCOMPRESSED, uncompressedSize)
.addAttribute(ATTRIBUTE_PREVIEW_FLAGS, previewFlags);
}
@Override

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2026, 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
@ -24,33 +24,34 @@
*/
package jdk.tools.jlink.internal;
import jdk.internal.jimage.ImageLocation;
import jdk.internal.jimage.ModuleLink;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
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.TreeSet;
import java.util.stream.Collectors;
/**
* A class to build a sorted tree of Resource paths as a tree of ImageLocation.
*
*/
// XXX Public only due to the JImageTask / JImageTask code duplication
public final class ImageResourcesTree {
public static boolean isTreeInfoResource(String path) {
return path.startsWith("/packages") || path.startsWith("/modules");
}
/**
* Path item tree node.
*/
private static class Node {
// Visible for testing only.
static class Node {
private final String name;
private final Map<String, Node> children = new TreeMap<>();
@ -66,6 +67,14 @@ public final class ImageResourcesTree {
}
}
private void setLocation(ImageLocationWriter loc) {
// This *can* be called more than once, but only with the same instance.
if (this.loc != null && loc != this.loc) {
throw new IllegalStateException("Cannot add different locations: " + name);
}
this.loc = Objects.requireNonNull(loc);
}
public String getPath() {
if (parent == null) {
return "/";
@ -95,215 +104,206 @@ public final class ImageResourcesTree {
}
}
private static final class ResourceNode extends Node {
// Visible for testing only.
static final class ResourceNode extends Node {
public ResourceNode(String name, Node parent) {
super(name, parent);
}
}
private static class PackageNode extends Node {
/**
* A reference to a package. Empty packages can be located inside one or
* more modules. A package with classes exist in only one module.
*/
static final class PackageReference {
/**
* A 2nd level package directory, {@code "/packages/<package-name>"}.
*
* <p>While package paths can exist within many modules, for each package
* there is at most one module in which that package has resources.
*
* <p>For example, the package path {@code java/util} exists in both the
* {@code java.base} and {@code java.logging} modules. This means both
* {@code "/packages/java.util/java.base"} and
* {@code "/packages/java.util/java.logging"} will exist, but only
* {@code "java.base"} entry will be marked as having content.
*
* <p>When processing module links in non-preview mode, entries marked
* as {@link ModuleLink#isPreviewOnly() preview-only} must be ignored.
*
* <p>If all links in a package are preview-only, then the entire package is
* marked as preview-only, and must be ignored.
*/
// Visible for testing only.
static final class PackageNode extends Node {
private final List<ModuleLink> moduleLinks;
private final String name;
private final boolean isEmpty;
PackageReference(String name, boolean isEmpty) {
this.name = Objects.requireNonNull(name);
this.isEmpty = isEmpty;
}
@Override
public String toString() {
return name + "[empty:" + isEmpty + "]";
}
}
private final Map<String, PackageReference> references = new TreeMap<>();
PackageNode(String name, Node parent) {
PackageNode(String name, List<ModuleLink> moduleLinks, Node parent) {
super(name, parent);
if (moduleLinks.isEmpty()) {
throw new IllegalStateException("Package must be associated with modules: " + name);
}
if (moduleLinks.stream().filter(ModuleLink::hasResources).count() > 1) {
throw new IllegalStateException("Multiple modules contain non-empty package: " + name);
}
this.moduleLinks = Collections.unmodifiableList(moduleLinks);
}
private void addReference(String name, boolean isEmpty) {
PackageReference ref = references.get(name);
if (ref == null || ref.isEmpty) {
references.put(name, new PackageReference(name, isEmpty));
}
List<ModuleLink> getModuleLinks() {
return moduleLinks;
}
}
private void validate() {
boolean exists = false;
for (PackageReference ref : references.values()) {
if (!ref.isEmpty) {
if (exists) {
throw new RuntimeException("Multiple modules to contain package "
+ getName());
} else {
exists = true;
}
}
}
// Not serialized, and never stored in any field of any class that is.
@SuppressWarnings("serial")
private static final class InvalidTreeException extends Exception {
public InvalidTreeException(Node badNode) {
super("Resources tree, invalid data structure, skipping: " + badNode.getPath());
}
// Exception only used for program flow, not debugging.
@Override
public Throwable fillInStackTrace() {return this;}
}
/**
* Tree of nodes.
*/
private static final class Tree {
// Visible for testing only.
static final class Tree {
private static final String PREVIEW_PREFIX = "META-INF/preview/";
private final Map<String, Node> directAccess = new HashMap<>();
private final List<String> paths;
private final Node root;
private Node modules;
private Node packages;
private Node packagesRoot;
private Tree(List<String> paths) {
this.paths = paths;
// Visible for testing only.
Tree(List<String> paths) {
this.paths = paths.stream().sorted(Comparator.reverseOrder()).toList();
// Root node is not added to the directAccess map.
root = new Node("", null);
buildTree();
}
private void buildTree() {
modules = new Node("modules", root);
directAccess.put(modules.getPath(), modules);
Node modulesRoot = new Node("modules", root);
directAccess.put(modulesRoot.getPath(), modulesRoot);
packagesRoot = new Node("packages", root);
directAccess.put(packagesRoot.getPath(), packagesRoot);
Map<String, Set<String>> moduleToPackage = new TreeMap<>();
Map<String, Set<String>> packageToModule = new TreeMap<>();
for (String p : paths) {
if (!p.startsWith("/")) {
continue;
}
String[] split = p.split("/");
// minimum length is 3 items: /<mod>/<pkg>
if (split.length < 3) {
System.err.println("Resources tree, invalid data structure, "
+ "skipping " + p);
continue;
}
Node current = modules;
String module = null;
for (int i = 0; i < split.length; i++) {
// When a non terminal node is marked as being a resource, something is wrong.
// Map of dot-separated package names to module links (those in
// which the package appear). Links are merged after to ensure each
// module name appears only once, but temporarily a module may have
// several link entries per package (e.g. with-content,
// without-content, normal, preview-only etc..).
Map<String, Set<ModuleLink>> packageToModules = new TreeMap<>();
for (String fullPath : paths) {
try {
processPath(fullPath, modulesRoot, packageToModules);
} catch (InvalidTreeException ex) {
// It has been observed some badly created jar file to contain
// invalid directory entry marled as not directory (see 8131762)
if (current instanceof ResourceNode) {
System.err.println("Resources tree, invalid data structure, "
+ "skipping " + p);
continue;
}
String s = split[i];
if (!s.isEmpty()) {
// First item, this is the module, simply add a new node to the
// tree.
if (module == null) {
module = s;
}
Node n = current.children.get(s);
if (n == null) {
if (i == split.length - 1) { // Leaf
n = new ResourceNode(s, current);
String pkg = toPackageName(n.parent);
//System.err.println("Adding a resource node. pkg " + pkg + ", name " + s);
if (pkg != null && !pkg.startsWith("META-INF")) {
Set<String> pkgs = moduleToPackage.get(module);
if (pkgs == null) {
pkgs = new TreeSet<>();
moduleToPackage.put(module, pkgs);
}
pkgs.add(pkg);
}
} else { // put only sub trees, no leaf
n = new Node(s, current);
directAccess.put(n.getPath(), n);
String pkg = toPackageName(n);
if (pkg != null && !pkg.startsWith("META-INF")) {
Set<String> mods = packageToModule.get(pkg);
if (mods == null) {
mods = new TreeSet<>();
packageToModule.put(pkg, mods);
}
mods.add(module);
}
}
}
current = n;
}
}
}
packages = new Node("packages", root);
directAccess.put(packages.getPath(), packages);
// The subset of package nodes that have some content.
// These packages exist only in a single module.
for (Map.Entry<String, Set<String>> entry : moduleToPackage.entrySet()) {
for (String pkg : entry.getValue()) {
PackageNode pkgNode = new PackageNode(pkg, packages);
pkgNode.addReference(entry.getKey(), false);
directAccess.put(pkgNode.getPath(), pkgNode);
// invalid directory entry marked as not directory (see 8131762).
System.err.println(ex.getMessage());
}
}
// All packages
for (Map.Entry<String, Set<String>> entry : packageToModule.entrySet()) {
// Do we already have a package node?
PackageNode pkgNode = (PackageNode) packages.getChildren(entry.getKey());
if (pkgNode == null) {
pkgNode = new PackageNode(entry.getKey(), packages);
}
for (String module : entry.getValue()) {
pkgNode.addReference(module, true);
}
// We've collected information for all "packages", including the root
// (empty) package and anything under "META-INF". However, these should
// not have entries in the "/packages" directory.
packageToModules.keySet().removeIf(p -> p.isEmpty() || p.equals("META-INF") || p.startsWith("META-INF."));
packageToModules.forEach((pkgName, modLinks) -> {
// Merge multiple links for the same module.
List<ModuleLink> pkgModules = modLinks.stream()
.collect(Collectors.groupingBy(ModuleLink::name))
.values().stream()
.map(links -> links.stream().reduce(ModuleLink::merge).orElseThrow())
.sorted()
.toList();
PackageNode pkgNode = new PackageNode(pkgName, pkgModules, packagesRoot);
directAccess.put(pkgNode.getPath(), pkgNode);
}
// Validate that the packages are well formed.
for (Node n : packages.children.values()) {
((PackageNode)n).validate();
}
});
}
public String toResourceName(Node node) {
private void processPath(
String fullPath,
Node modulesRoot,
Map<String, Set<ModuleLink>> packageToModules)
throws InvalidTreeException {
// Paths are untrusted, so be careful about checking expected format.
if (!fullPath.startsWith("/") || fullPath.endsWith("/") || fullPath.contains("//")) {
return;
}
int modEnd = fullPath.indexOf('/', 1);
// Ensure non-empty module name with non-empty suffix.
if (modEnd <= 1) {
return;
}
String modName = fullPath.substring(1, modEnd);
String pkgPath = fullPath.substring(modEnd + 1);
Node parentNode = getDirectoryNode(modName, modulesRoot);
boolean isPreviewPath = false;
if (pkgPath.startsWith(PREVIEW_PREFIX)) {
// For preview paths, process nodes relative to the preview directory.
pkgPath = pkgPath.substring(PREVIEW_PREFIX.length());
Node metaInf = getDirectoryNode("META-INF", parentNode);
parentNode = getDirectoryNode("preview", metaInf);
isPreviewPath = true;
}
int pathEnd = pkgPath.lastIndexOf('/');
// From invariants tested above, this must now be well-formed.
String fullPkgName = (pathEnd == -1) ? "" : pkgPath.substring(0, pathEnd).replace('/', '.');
String resourceName = pkgPath.substring(pathEnd + 1);
// Intermediate packages are marked "empty" (no resources). This might
// later be merged with a non-empty link for the same package.
ModuleLink emptyLink = ModuleLink.forEmptyPackage(modName, isPreviewPath);
// Work down through empty packages to final resource.
for (int i = pkgEndIndex(fullPkgName, 0); i != -1; i = pkgEndIndex(fullPkgName, i)) {
// Due to invariants already checked, pkgName is non-empty.
String pkgName = fullPkgName.substring(0, i);
packageToModules.computeIfAbsent(pkgName, p -> new HashSet<>()).add(emptyLink);
String childNodeName = pkgName.substring(pkgName.lastIndexOf('.') + 1);
parentNode = getDirectoryNode(childNodeName, parentNode);
}
// Reached non-empty (leaf) package (could still be a duplicate).
Node resourceNode = parentNode.getChildren(resourceName);
if (resourceNode == null) {
ModuleLink resourceLink = ModuleLink.forPackage(modName, isPreviewPath);
packageToModules.computeIfAbsent(fullPkgName, p -> new HashSet<>()).add(resourceLink);
// Init adds new node to parent (don't add resources to directAccess).
new ResourceNode(resourceName, parentNode);
} else if (!(resourceNode instanceof ResourceNode)) {
throw new InvalidTreeException(resourceNode);
}
}
private Node getDirectoryNode(String name, Node parent) throws InvalidTreeException {
Node child = parent.getChildren(name);
if (child == null) {
// Adds child to parent during init.
child = new Node(name, parent);
directAccess.put(child.getPath(), child);
} else if (child instanceof ResourceNode) {
throw new InvalidTreeException(child);
}
return child;
}
// Helper to iterate package names up to, and including, the complete name.
private int pkgEndIndex(String s, int i) {
if (i >= 0 && i < s.length()) {
i = s.indexOf('.', i + 1);
return i != -1 ? i : s.length();
}
return -1;
}
private String toResourceName(Node node) {
if (!node.children.isEmpty()) {
throw new RuntimeException("Node is not a resource");
}
return removeRadical(node);
}
public String getModule(Node node) {
if (node.parent == null || node.getName().equals("modules")
|| node.getName().startsWith("packages")) {
return null;
}
String path = removeRadical(node);
// "/xxx/...";
path = path.substring(1);
int i = path.indexOf("/");
if (i == -1) {
return path;
} else {
return path.substring(0, i);
}
}
public String toPackageName(Node node) {
if (node.parent == null) {
return null;
}
String path = removeRadical(node.getPath(), "/modules/");
String module = getModule(node);
if (path.equals(module)) {
return null;
}
String pkg = removeRadical(path, module + "/");
return pkg.replace('/', '.');
}
public String removeRadical(Node node) {
private String removeRadical(Node node) {
return removeRadical(node.getPath(), "/modules");
}
@ -339,9 +339,10 @@ public final class ImageResourcesTree {
private int addLocations(Node current) {
if (current instanceof PackageNode) {
PackageNode pkgNode = (PackageNode) current;
int size = pkgNode.references.size() * 8;
writer.addLocation(current.getPath(), offset, 0, size);
List<ModuleLink> links = ((PackageNode) current).getModuleLinks();
// "/packages/<pkg name>" entries have 8-byte entries (flags+offset).
int size = links.size() * 8;
writer.addLocation(current.getPath(), offset, 0, size, ImageLocation.getPackageFlags(links));
offset += size;
} else {
int[] ret = new int[current.children.size()];
@ -351,8 +352,10 @@ public final class ImageResourcesTree {
i += 1;
}
if (current != tree.getRoot() && !(current instanceof ResourceNode)) {
int locFlags = ImageLocation.getPreviewFlags(current.getPath(), tree.directAccess::containsKey);
// Normal directory entries have 4-byte entries (offset only).
int size = ret.length * 4;
writer.addLocation(current.getPath(), offset, 0, size);
writer.addLocation(current.getPath(), offset, 0, size, locFlags);
offset += size;
}
}
@ -369,7 +372,7 @@ public final class ImageResourcesTree {
for (Map.Entry<String, ImageLocationWriter> entry : outLocations.entrySet()) {
Node item = tree.getMap().get(entry.getKey());
if (item != null) {
item.loc = entry.getValue();
item.setLocation(entry.getValue());
}
}
computeContent(tree.getRoot(), outLocations);
@ -378,18 +381,13 @@ public final class ImageResourcesTree {
private int computeContent(Node current, Map<String, ImageLocationWriter> outLocations) {
if (current instanceof PackageNode) {
// /packages/<pkg name>
PackageNode pkgNode = (PackageNode) current;
int size = pkgNode.references.size() * 8;
ByteBuffer buff = ByteBuffer.allocate(size);
buff.order(writer.getByteOrder());
for (PackageNode.PackageReference mod : pkgNode.references.values()) {
buff.putInt(mod.isEmpty ? 1 : 0);
buff.putInt(writer.addString(mod.name));
}
byte[] arr = buff.array();
content.add(arr);
current.loc = outLocations.get(current.getPath());
// "/packages/<pkg name>" entries have 8-byte entries (flags+offset).
List<ModuleLink> links = ((PackageNode) current).getModuleLinks();
ByteBuffer byteBuffer = ByteBuffer.allocate(8 * links.size());
byteBuffer.order(writer.getByteOrder());
ModuleLink.write(links, byteBuffer.asIntBuffer(), writer::addString);
content.add(byteBuffer.array());
current.setLocation(outLocations.get(current.getPath()));
} else {
int[] ret = new int[current.children.size()];
int i = 0;
@ -410,10 +408,10 @@ public final class ImageResourcesTree {
if (current instanceof ResourceNode) {
// A resource location, remove "/modules"
String s = tree.toResourceName(current);
current.loc = outLocations.get(s);
current.setLocation(outLocations.get(s));
} else {
// empty "/packages" or empty "/modules" paths
current.loc = outLocations.get(current.getPath());
current.setLocation(outLocations.get(current.getPath()));
}
}
if (current.loc == null && current != tree.getRoot()) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2026, 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 @@ import jdk.internal.jimage.ImageStringsReader;
class ImageStringsWriter implements ImageStrings {
private static final int NOT_FOUND = -1;
static final int EMPTY_OFFSET = 0;
private final HashMap<String, Integer> stringToOffsetMap;
private final ImageStream stream;
@ -42,16 +41,20 @@ 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.
// New strings can be reserved after existing strings without having to
// change the jimage file version, but any change to existing entries
// requires the jimage file version to be increased at the same time.
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 + "]");
}
}

View File

@ -1,5 +1,6 @@
/*
* Copyright (c) 2024, Red Hat, Inc.
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -32,7 +33,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@ -51,6 +51,8 @@ import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.internal.jimage.ResourceEntries;
import jdk.internal.jimage.SystemImageReader;
import jdk.internal.util.OperatingSystem;
import jdk.tools.jlink.internal.Archive.Entry.EntryType;
import jdk.tools.jlink.internal.runtimelink.ResourceDiff;
@ -63,10 +65,9 @@ import jdk.tools.jlink.plugin.ResourcePoolEntry.Type;
* associated files from the filesystem of the JDK installation.
*/
public class JRTArchive implements Archive {
private final String module;
private final Path path;
private final ModuleReference ref;
private final ResourceEntries imageResources;
// The collection of files of this module
private final List<JRTFile> files = new ArrayList<>();
// Files not part of the lib/modules image of the JDK install.
@ -99,12 +100,11 @@ public class JRTArchive implements Archive {
Set<String> upgradeableFiles) {
this.module = module;
this.path = path;
this.ref = ModuleFinder.ofSystem()
.find(module)
.orElseThrow(() ->
new IllegalArgumentException(
"Module " + module +
" not part of the JDK install"));
ModuleFinder.ofSystem()
.find(module)
.orElseThrow(() -> new IllegalArgumentException(
"Module " + module + " not part of the JDK install"));
this.imageResources = SystemImageReader.getResourceEntries();
this.errorOnModifiedFile = errorOnModifiedFile;
this.otherRes = readModuleResourceFile(module);
this.resDiff = Objects.requireNonNull(perModDiff).stream()
@ -159,52 +159,35 @@ public class JRTArchive implements Archive {
Objects.equals(path, other.path));
}
private boolean isNormalOrModifiedDiff(String name) {
ResourceDiff rd = resDiff.get(name);
// Filter all resources with a resource diff of kind MODIFIED.
// Note that REMOVED won't happen since in that case the module listing
// won't have the resource anyway.
// Note as well that filter removes files of kind ADDED. Those files are
// not in the packaged modules, so ought not to get returned from the
// pipeline.
return (rd == null || rd.getKind() == ResourceDiff.Kind.MODIFIED);
}
private void collectFiles() throws IOException {
if (files.isEmpty()) {
addNonClassResources();
// Add classes/resources from the run-time image,
// patched with the run-time image diff
files.addAll(ref.open().list()
.filter(i -> {
String lookupKey = String.format("/%s/%s", module, i);
ResourceDiff rd = resDiff.get(lookupKey);
// Filter all resources with a resource diff
// that are of kind MODIFIED.
// Note that REMOVED won't happen since in
// that case the module listing won't have
// the resource anyway.
// Note as well that filter removes files
// of kind ADDED. Those files are not in
// the packaged modules, so ought not to
// get returned from the pipeline.
return (rd == null ||
rd.getKind() == ResourceDiff.Kind.MODIFIED);
})
.map(s -> {
String lookupKey = String.format("/%s/%s", module, s);
return new JRTArchiveFile(JRTArchive.this, s,
EntryType.CLASS_OR_RESOURCE,
null /* hashOrTarget */,
false /* symlink */,
resDiff.get(lookupKey));
})
.toList());
imageResources.getEntryNames(module)
.filter(this::isNormalOrModifiedDiff)
.sorted()
.map(name -> new JrtClassOrResource(this, name, resDiff.get(name)))
.forEach(files::add);
// Finally add all files only present in the resource diff
// That is, removed items in the run-time image.
files.addAll(resDiff.values().stream()
.filter(rd -> rd.getKind() == ResourceDiff.Kind.REMOVED)
.map(s -> {
int secondSlash = s.getName().indexOf("/", 1);
assert secondSlash != -1;
String pathWithoutModule = s.getName().substring(secondSlash + 1);
return new JRTArchiveFile(JRTArchive.this,
pathWithoutModule,
EntryType.CLASS_OR_RESOURCE,
null /* hashOrTarget */,
false /* symlink */,
s);
})
.toList());
.filter(rd -> rd.getKind() == ResourceDiff.Kind.REMOVED)
.map(rd -> new JrtClassOrResource(this, rd.getName(), rd))
.toList());
}
}
@ -234,15 +217,10 @@ public class JRTArchive implements Archive {
}
}
return new JRTArchiveFile(JRTArchive.this,
m.resPath,
toEntryType(m.resType),
m.hashOrTarget,
m.symlink,
/* diff only for resources */
null);
})
.toList());
return new JrtOtherFile(
this, m.resPath, toEntryType(m.resType), m.hashOrTarget, m.symlink);
})
.toList());
}
}
@ -323,11 +301,11 @@ public class JRTArchive implements Archive {
resPath);
}
/**
/*
* line: <int>|<int>|<hashOrTarget>|<path>
*
* Take the integer before '|' convert it to a Type. The second
* token is an integer representing symlinks (or not). The third token is
* Take the integer before '|' convert it to a Type. The second token
* is an integer representing symlinks (or not). The third token is
* a hash sum (sha512) of the file denoted by the fourth token (path).
*/
static ResourceFileEntry decodeFromString(String line) {
@ -437,47 +415,75 @@ public class JRTArchive implements Archive {
Entry toEntry();
}
record JRTArchiveFile(Archive archive,
String resPath,
EntryType resType,
String sha,
boolean symlink,
ResourceDiff diff) implements JRTFile {
record JrtClassOrResource(
JRTArchive archive,
String resPath,
ResourceDiff diff) implements JRTFile {
@Override
public Entry toEntry() {
return new Entry(archive,
String.format("/%s/%s",
archive.moduleName(),
resPath),
resPath,
resType) {
assert resPath.startsWith("/" + archive.moduleName() + "/");
String resName = resPath.substring(archive.moduleName().length() + 2);
// If the resource has a diff to the packaged modules, use the diff.
// Diffs of kind ADDED have been filtered out in collectFiles();
if (diff != null) {
assert diff.getKind() != ResourceDiff.Kind.ADDED;
assert diff.getName().equals(resPath);
return new Entry(archive, resPath, resName, EntryType.CLASS_OR_RESOURCE) {
@Override
public long size() {
return diff.getResourceBytes().length;
}
@Override
public InputStream stream() {
return new ByteArrayInputStream(diff.getResourceBytes());
}
};
} else {
return new Entry(archive, resPath, resName, EntryType.CLASS_OR_RESOURCE) {
@Override
public long size() {
return archive.imageResources.getSize(resPath);
}
@Override
public InputStream stream() {
// Byte content could be cached in the entry if needed.
return new ByteArrayInputStream(archive.imageResources.getBytes(resPath));
}
};
}
}
}
record JrtOtherFile(
JRTArchive archive,
String resPath,
EntryType resType,
String sha,
boolean symlink) implements JRTFile {
// Read from the base JDK image, special casing
// symlinks, which have the link target in the
// hashOrTarget field.
Path targetPath() {
return BASE.resolve(symlink ? sha : resPath);
}
public Entry toEntry() {
assert resType != EntryType.CLASS_OR_RESOURCE;
return new Entry(
archive,
String.format("/%s/%s", archive.moduleName(), resPath),
resPath,
resType) {
@Override
public long size() {
try {
if (resType != EntryType.CLASS_OR_RESOURCE) {
// Read from the base JDK image, special casing
// symlinks, which have the link target in the
// hashOrTarget field
if (symlink) {
return Files.size(BASE.resolve(sha));
}
return Files.size(BASE.resolve(resPath));
} else {
if (diff != null) {
// If the resource has a diff to the
// packaged modules, use the diff. Diffs of kind
// ADDED have been filtered out in collectFiles();
assert diff.getKind() != ResourceDiff.Kind.ADDED;
assert diff.getName().equals(String.format("/%s/%s",
archive.moduleName(),
resPath));
return diff.getResourceBytes().length;
}
// Read from the module image. This works, because
// the underlying base path is a JrtPath with the
// JrtFileSystem underneath which is able to handle
// this size query.
return Files.size(archive.getPath().resolve(resPath));
}
return Files.size(targetPath());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
@ -485,28 +491,8 @@ public class JRTArchive implements Archive {
@Override
public InputStream stream() throws IOException {
if (resType != EntryType.CLASS_OR_RESOURCE) {
// Read from the base JDK image.
Path path = symlink ? BASE.resolve(sha) : BASE.resolve(resPath);
return Files.newInputStream(path);
} else {
// Read from the module image. Use the diff to the
// packaged modules if we have one. Diffs of kind
// ADDED have been filtered out in collectFiles();
if (diff != null) {
assert diff.getKind() != ResourceDiff.Kind.ADDED;
assert diff.getName().equals(String.format("/%s/%s",
archive.moduleName(),
resPath));
return new ByteArrayInputStream(diff.getResourceBytes());
}
String module = archive.moduleName();
ModuleReference mRef = ModuleFinder.ofSystem()
.find(module).orElseThrow();
return mRef.open().open(resPath).orElseThrow();
}
return Files.newInputStream(targetPath());
}
};
}
}

View File

@ -373,21 +373,22 @@ public class JlinkTask {
plugins = plugins == null ? new PluginsConfiguration() : plugins;
// First create the image provider
ImageProvider imageProvider =
createImageProvider(config,
null,
IGNORE_SIGNING_DEFAULT,
false,
null,
false,
new OptionsValues(),
null);
try (ImageHelper imageProvider =
createImageProvider(config,
null,
IGNORE_SIGNING_DEFAULT,
false,
null,
false,
new OptionsValues(),
null)) {
// Then create the Plugin Stack
ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(plugins);
// Then create the Plugin Stack
ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(plugins);
//Ask the stack to proceed;
stack.operate(imageProvider);
// Ask the stack to proceed;
stack.operate(imageProvider);
}
}
// the token for "all modules on the module path"
@ -511,22 +512,24 @@ public class JlinkTask {
}
// First create the image provider
ImageHelper imageProvider = createImageProvider(config,
options.packagedModulesPath,
options.ignoreSigning,
options.bindServices,
options.endian,
options.verbose,
options,
log);
try (ImageHelper imageProvider = createImageProvider(config,
options.packagedModulesPath,
options.ignoreSigning,
options.bindServices,
options.endian,
options.verbose,
options,
log)) {
// Then create the Plugin Stack
ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(
taskHelper.getPluginsConfig(
options.output,
options.launchers,
imageProvider.targetPlatform));
// Then create the Plugin Stack
ImagePluginStack stack = ImagePluginConfiguration.parseConfiguration(
taskHelper.getPluginsConfig(options.output, options.launchers,
imageProvider.targetPlatform));
//Ask the stack to proceed
stack.operate(imageProvider);
//Ask the stack to proceed
stack.operate(imageProvider);
}
}
/**
@ -1054,10 +1057,11 @@ public class JlinkTask {
return sb.toString();
}
private static record ImageHelper(Set<Archive> archives,
Platform targetPlatform,
Path packagedModulesPath,
boolean generateRuntimeImage) implements ImageProvider {
private record ImageHelper(Set<Archive> archives,
Platform targetPlatform,
Path packagedModulesPath,
boolean generateRuntimeImage)
implements ImageProvider, AutoCloseable {
@Override
public ExecutableImage retrieve(ImagePluginStack stack) throws IOException {
ExecutableImage image = ImageFileCreator.create(archives,
@ -1073,5 +1077,25 @@ public class JlinkTask {
}
return image;
}
@Override
public void close() throws IOException {
List<IOException> thrown = null;
for (Archive archive : archives) {
try {
archive.close();
} catch (IOException ex) {
if (thrown == null) {
thrown = new ArrayList<>();
}
thrown.add(ex);
}
}
if (thrown != null) {
IOException ex = new IOException("Archives could not be closed", thrown.getFirst());
thrown.subList(1, thrown.size()).forEach(ex::addSuppressed);
throw ex;
}
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2026, 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
@ -66,20 +66,8 @@ public class ResourcePoolManager {
}
}
/**
* Returns true if a resource is located in a named package.
*/
public static boolean isNamedPackageResource(String name) {
int index = name.lastIndexOf("/");
if (index == -1) {
return false;
} else {
String pn = name.substring(0, index).replace('/', '.');
return Checks.isPackageName(pn);
}
}
static class ResourcePoolModuleImpl implements ResourcePoolModule {
private static final String PREVIEW_PREFIX = "META-INF/preview/";
final Map<String, ResourcePoolEntry> moduleContent = new LinkedHashMap<>();
// lazily initialized
@ -132,16 +120,8 @@ public class ResourcePoolManager {
public Set<String> packages() {
Set<String> pkgs = new HashSet<>();
moduleContent.values().stream()
.filter(m -> m.type() == ResourcePoolEntry.Type.CLASS_OR_RESOURCE)
.forEach(res -> {
String name = ImageFileCreator.resourceName(res.path());
if (isNamedPackageResource(name)) {
String pkg = ImageFileCreator.toPackage(name);
if (!pkg.isEmpty()) {
pkgs.add(pkg);
}
}
});
.filter(m -> m.type() == ResourcePoolEntry.Type.CLASS_OR_RESOURCE)
.forEach(res -> inferPackageName(res).ifPresent(pkgs::add));
return pkgs;
}
@ -159,6 +139,39 @@ public class ResourcePoolManager {
public int entryCount() {
return moduleContent.values().size();
}
/**
* Returns a valid non-empty package name, inferred from a resource pool
* entry's path.
*
* <p>If the resource pool entry is for a preview resource (i.e. with
* path {@code "/mod-name/META-INF/preview/pkg-path/resource-name"})
* the package name is the non-preview name based on {@code "pkg-path"}.
*
* @return the inferred package name, or {@link Optional#empty() empty}
* if no name could be inferred.
*/
private static Optional<String> inferPackageName(ResourcePoolEntry res) {
// Expect entry paths to be "/mod-name/pkg-path/resource-name", but
// may also get "/mod-name/META-INF/preview/pkg-path/resource-name"
String name = res.path();
if (name.charAt(0) == '/') {
int pkgStart = name.indexOf('/', 1) + 1;
int pkgEnd = name.lastIndexOf('/');
if (pkgStart > 0 && pkgEnd > pkgStart) {
String pkgPath = name.substring(pkgStart, pkgEnd);
// Handle preview paths by removing the prefix.
if (pkgPath.startsWith(PREVIEW_PREFIX)) {
pkgPath = pkgPath.substring(PREVIEW_PREFIX.length());
}
String pkgName = pkgPath.replace('/', '.');
if (Checks.isPackageName(pkgName)) {
return Optional.of(pkgName);
}
}
}
return Optional.empty();
}
}
public class ResourcePoolImpl implements ResourcePool {

View File

@ -0,0 +1,135 @@
/*
* Copyright (c) 2025, 2026, 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.ImageLocation;
import jdk.internal.jimage.ModuleLink;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
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 ImageLocation.
* @modules java.base/jdk.internal.jimage
* @run junit/othervm -esa ImageLocationTest
*/
public class ImageLocationTest {
@ParameterizedTest
@ValueSource(strings = {
"/modules/modfoo/com",
"/modules/modfoo/com/foo/Foo.class"})
public void getFlags_resourceNames(String name) {
String previewName = previewName(name);
int noPreviewFlags =
ImageLocation.getPreviewFlags(name, Set.of(name)::contains);
assertEquals(0, noPreviewFlags);
assertFalse(ImageLocation.hasPreviewVersion(noPreviewFlags));
assertFalse(ImageLocation.isPreviewOnly(noPreviewFlags));
int withPreviewFlags =
ImageLocation.getPreviewFlags(name, Set.of(name, previewName)::contains);
assertTrue(ImageLocation.hasPreviewVersion(withPreviewFlags));
assertFalse(ImageLocation.isPreviewOnly(withPreviewFlags));
int previewOnlyFlags = ImageLocation.getPreviewFlags(previewName, Set.of(previewName)::contains);
assertFalse(ImageLocation.hasPreviewVersion(previewOnlyFlags));
assertTrue(ImageLocation.isPreviewOnly(previewOnlyFlags));
}
@ParameterizedTest
@ValueSource(strings = {
"/modules",
"/packages",
"/modules/modfoo",
"/modules/modfoo/META-INF",
"/modules/modfoo/META-INF/module-info.class"})
public void getFlags_zero(String name) {
assertEquals(0, ImageLocation.getPreviewFlags(name, Set.of(name)::contains));
}
@Test
public void getFlags_packageFlags() {
assertThrows(
IllegalArgumentException.class,
() -> ImageLocation.getPreviewFlags("/packages/pkgname", p -> true));
}
@Test
public void getPackageFlags_noPreview() {
List<ModuleLink> links = List.of(
ModuleLink.forPackage("modfoo", false),
ModuleLink.forEmptyPackage("modbar", false),
ModuleLink.forEmptyPackage("modbaz", false));
int noPreviewFlags = ImageLocation.getPackageFlags(links);
assertEquals(0, noPreviewFlags);
assertFalse(ImageLocation.hasPreviewVersion(noPreviewFlags));
assertFalse(ImageLocation.isPreviewOnly(noPreviewFlags));
}
@Test
public void getPackageFlags_withPreview() {
List<ModuleLink> links = List.of(
ModuleLink.forPackage("modfoo", true),
ModuleLink.forEmptyPackage("modbar", false),
ModuleLink.forEmptyPackage("modbaz", true));
int withPreviewFlags = ImageLocation.getPackageFlags(links);
assertTrue(ImageLocation.hasPreviewVersion(withPreviewFlags));
assertFalse(ImageLocation.isPreviewOnly(withPreviewFlags));
}
@Test
public void getPackageFlags_previewOnly() {
List<ModuleLink> links = List.of(
ModuleLink.forPackage("modfoo", true),
ModuleLink.forEmptyPackage("modbar", true),
ModuleLink.forEmptyPackage("modbaz", true));
int previewOnlyFlags = ImageLocation.getPackageFlags(links);
// Note the asymmetry between this and the getFlags() case. Unlike
// module resources, there is no concept of a separate package directory
// existing in the preview namespace, so a single entry serves both
// purposes, and hasPreviewVersion() and isPreviewOnly() can both be set.
assertTrue(ImageLocation.hasPreviewVersion(previewOnlyFlags));
assertTrue(ImageLocation.isPreviewOnly(previewOnlyFlags));
}
private static final Pattern MODULES_PATH = Pattern.compile("/modules/([^/]+)/(.+)");
private static String previewName(String name) {
var m = MODULES_PATH.matcher(name);
if (m.matches() && !m.group(2).startsWith("/META-INF/preview/")) {
return "/modules/" + m.group(1) + "/META-INF/preview/" + m.group(2);
}
throw new IllegalStateException("Invalid modules name: " + name);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -23,6 +23,7 @@
import jdk.internal.jimage.ImageReader;
import jdk.internal.jimage.ImageReader.Node;
import jdk.internal.jimage.PreviewMode;
import jdk.test.lib.compiler.InMemoryJavaCompiler;
import jdk.test.lib.util.JarBuilder;
import jdk.tools.jlink.internal.LinkableRuntimeImage;
@ -43,6 +44,7 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toSet;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -63,22 +65,33 @@ import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
* @library /test/jdk/tools/lib
* /test/lib
* @build tests.*
* @run junit/othervm ImageReaderTest
* @run junit/othervm -esa ImageReaderTest
*/
/// Using PER_CLASS lifecycle means the (expensive) image file is only build once.
/// Using PER_CLASS lifecycle means the (expensive) image file is only built once.
/// There is no mutable test instance state to worry about.
@TestInstance(PER_CLASS)
public class ImageReaderTest {
// The '@' prefix marks the entry as a preview entry which will be placed in
// the '/modules/<module>/META-INF/preview/...' namespace.
private static final Map<String, List<String>> IMAGE_ENTRIES = Map.of(
"modfoo", Arrays.asList(
"com.foo.Alpha",
"com.foo.Beta",
"com.foo.bar.Gamma"),
"com.foo.HasPreviewVersion",
"com.foo.NormalFoo",
"com.foo.bar.NormalBar",
// Replaces original class in preview mode.
"@com.foo.HasPreviewVersion",
// New class in existing package in preview mode.
"@com.foo.bar.IsPreviewOnly"),
"modbar", Arrays.asList(
"com.bar.One",
"com.bar.Two"));
"com.bar.Two",
// Two new packages in preview mode (new symbolic links).
"@com.bar.preview.stuff.Foo",
"@com.bar.preview.stuff.Bar"),
"modgus", Arrays.asList(
// A second module with a preview-only empty package (preview).
"@com.bar.preview.other.Gus"));
private final Path image = buildJImage(IMAGE_ENTRIES);
@ParameterizedTest
@ -91,7 +104,7 @@ public class ImageReaderTest {
"/modules/modfoo/com/foo",
"/modules/modfoo/com/foo/bar"})
public void testModuleDirectories_expected(String name) throws IOException {
try (ImageReader reader = ImageReader.open(image)) {
try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
assertDir(reader, name);
}
}
@ -106,38 +119,40 @@ public class ImageReaderTest {
"/modules/modfoo//com",
"/modules/modfoo/com/"})
public void testModuleNodes_absent(String name) throws IOException {
try (ImageReader reader = ImageReader.open(image)) {
try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
assertAbsent(reader, name);
}
}
@Test
public void testModuleResources() throws IOException {
try (ImageReader reader = ImageReader.open(image)) {
assertNode(reader, "/modules/modfoo/com/foo/Alpha.class");
try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
assertNode(reader, "/modules/modfoo/com/foo/HasPreviewVersion.class");
assertNode(reader, "/modules/modbar/com/bar/One.class");
ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
assertEquals("Class: com.foo.Alpha", loader.loadAndGetToString("modfoo", "com.foo.Alpha"));
assertEquals("Class: com.foo.Beta", loader.loadAndGetToString("modfoo", "com.foo.Beta"));
assertEquals("Class: com.foo.bar.Gamma", loader.loadAndGetToString("modfoo", "com.foo.bar.Gamma"));
assertEquals("Class: com.bar.One", loader.loadAndGetToString("modbar", "com.bar.One"));
assertNonPreviewVersion(loader, "modfoo", "com.foo.HasPreviewVersion");
assertNonPreviewVersion(loader, "modfoo", "com.foo.NormalFoo");
assertNonPreviewVersion(loader, "modfoo", "com.foo.bar.NormalBar");
assertNonPreviewVersion(loader, "modbar", "com.bar.One");
}
}
@ParameterizedTest
@CsvSource(delimiter = ':', value = {
"modfoo:com/foo/Alpha.class",
"modfoo:com/foo/HasPreviewVersion.class",
"modbar:com/bar/One.class",
})
public void testResource_present(String modName, String resPath) throws IOException {
try (ImageReader reader = ImageReader.open(image)) {
assertNotNull(reader.findResourceNode(modName, resPath));
assertTrue(reader.containsResource(modName, resPath));
for (PreviewMode mode : List.of(PreviewMode.ENABLED, PreviewMode.DISABLED)) {
try (ImageReader reader = ImageReader.open(image, mode)) {
assertNotNull(reader.findResourceNode(modName, resPath));
assertTrue(reader.containsResource(modName, resPath));
String canonicalNodeName = "/modules/" + modName + "/" + resPath;
Node node = reader.findNode(canonicalNodeName);
assertTrue(node != null && node.isResource());
String canonicalNodeName = "/modules/" + modName + "/" + resPath;
Node node = reader.findNode(canonicalNodeName);
assertTrue(node != null && node.isResource());
}
}
}
@ -147,26 +162,31 @@ public class ImageReaderTest {
"modfoo:/com/bar/One.class",
// Resource in wrong module.
"modfoo:com/bar/One.class",
"modbar:com/foo/Alpha.class",
"modbar:com/foo/HasPreviewVersion.class",
// Directories are not returned.
"modfoo:com/foo",
"modbar:com/bar",
// JImage entries exist for these, but they are not resources.
"modules:modfoo/com/foo/Alpha.class",
"modules:modfoo/com/foo/HasPreviewVersion.class",
"packages:com.foo/modfoo",
// Empty module names/paths do not find resources.
"'':modfoo/com/foo/Alpha.class",
"modfoo:''"})
"'':modfoo/com/foo/HasPreviewVersion.class",
"modfoo:''",
// Make sure preview paths are excluded.
"modfoo:META-INF/preview/com/foo/HasPreviewVersion.class",
})
public void testResource_absent(String modName, String resPath) throws IOException {
try (ImageReader reader = ImageReader.open(image)) {
assertNull(reader.findResourceNode(modName, resPath));
assertFalse(reader.containsResource(modName, resPath));
for (PreviewMode mode : List.of(PreviewMode.ENABLED, PreviewMode.DISABLED)) {
try (ImageReader reader = ImageReader.open(image, mode)) {
assertNull(reader.findResourceNode(modName, resPath));
assertFalse(reader.containsResource(modName, resPath));
// Non-existent resources names should either not be found,
// or (in the case of directory nodes) not be resources.
String canonicalNodeName = "/modules/" + modName + "/" + resPath;
Node node = reader.findNode(canonicalNodeName);
assertTrue(node == null || !node.isResource());
// Non-existent resources names should either not be found,
// or (in the case of directory nodes) not be resources.
String canonicalNodeName = "/modules/" + modName + "/" + resPath;
Node node = reader.findNode(canonicalNodeName);
assertTrue(node == null || !node.isResource());
}
}
}
@ -175,10 +195,10 @@ public class ImageReaderTest {
// Don't permit module names to contain paths.
"modfoo/com/bar:One.class",
"modfoo/com:bar/One.class",
"modules/modfoo/com:foo/Alpha.class",
"modules/modfoo/com:foo/HasPreviewVersion.class",
})
public void testResource_invalid(String modName, String resPath) throws IOException {
try (ImageReader reader = ImageReader.open(image)) {
try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
assertThrows(IllegalArgumentException.class, () -> reader.containsResource(modName, resPath));
assertThrows(IllegalArgumentException.class, () -> reader.findResourceNode(modName, resPath));
}
@ -186,9 +206,9 @@ public class ImageReaderTest {
@Test
public void testPackageDirectories() throws IOException {
try (ImageReader reader = ImageReader.open(image)) {
try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
Node root = assertDir(reader, "/packages");
Set<String> pkgNames = root.getChildNames().collect(Collectors.toSet());
Set<String> pkgNames = root.getChildNames().collect(toSet());
assertTrue(pkgNames.contains("/packages/com"));
assertTrue(pkgNames.contains("/packages/com.foo"));
assertTrue(pkgNames.contains("/packages/com.bar"));
@ -203,7 +223,7 @@ public class ImageReaderTest {
@Test
public void testPackageLinks() throws IOException {
try (ImageReader reader = ImageReader.open(image)) {
try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
Node moduleFoo = assertDir(reader, "/modules/modfoo");
Node moduleBar = assertDir(reader, "/modules/modbar");
assertSame(assertLink(reader, "/packages/com.foo/modfoo").resolveLink(), moduleFoo);
@ -211,6 +231,123 @@ public class ImageReaderTest {
}
}
@Test
public void testPreviewResources_disabled() throws IOException {
try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
// No preview classes visible.
assertNonPreviewVersion(loader, "modfoo", "com.foo.HasPreviewVersion");
assertNonPreviewVersion(loader, "modfoo", "com.foo.NormalFoo");
assertNonPreviewVersion(loader, "modfoo", "com.foo.bar.NormalBar");
// NormalBar exists but IsPreviewOnly doesn't.
assertResource(reader, "modfoo", "com/foo/bar/NormalBar.class");
assertAbsent(reader, "/modules/modfoo/com/foo/bar/IsPreviewOnly.class");
assertDirContents(reader, "/modules/modfoo/com/foo", "HasPreviewVersion.class", "NormalFoo.class", "bar");
assertDirContents(reader, "/modules/modfoo/com/foo/bar", "NormalBar.class");
}
}
@Test
public void testPreviewResources_enabled() throws IOException {
try (ImageReader reader = ImageReader.open(image, PreviewMode.ENABLED)) {
ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
// Preview version of classes either overwrite existing entries or are added to directories.
assertPreviewVersion(loader, "modfoo", "com.foo.HasPreviewVersion");
assertNonPreviewVersion(loader, "modfoo", "com.foo.NormalFoo");
assertNonPreviewVersion(loader, "modfoo", "com.foo.bar.NormalBar");
assertPreviewVersion(loader, "modfoo", "com.foo.bar.IsPreviewOnly");
// Both NormalBar and IsPreviewOnly exist (direct lookup and as child nodes).
assertResource(reader, "modfoo", "com/foo/bar/NormalBar.class");
assertResource(reader, "modfoo", "com/foo/bar/IsPreviewOnly.class");
assertDirContents(reader, "/modules/modfoo/com/foo", "HasPreviewVersion.class", "NormalFoo.class", "bar");
assertDirContents(reader, "/modules/modfoo/com/foo/bar", "NormalBar.class", "IsPreviewOnly.class");
}
}
@Test
public void testPreviewOnlyPackages_disabled() throws IOException {
try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
// No 'preview' package or anything inside it.
assertDirContents(reader, "/modules/modbar/com/bar", "One.class", "Two.class");
assertAbsent(reader, "/modules/modbar/com/bar/preview");
assertAbsent(reader, "/modules/modbar/com/bar/preview/stuff/Foo.class");
// And no package link.
assertAbsent(reader, "/packages/com.bar.preview");
}
}
@Test
public void testPreviewOnlyPackages_enabled() throws IOException {
try (ImageReader reader = ImageReader.open(image, PreviewMode.ENABLED)) {
ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
// In preview mode 'preview' package exists with preview only content.
assertDirContents(reader, "/modules/modbar/com/bar", "One.class", "Two.class", "preview");
assertDirContents(reader, "/modules/modbar/com/bar/preview/stuff", "Foo.class", "Bar.class");
assertResource(reader, "modbar", "com/bar/preview/stuff/Foo.class");
// And package links exists.
assertDirContents(reader, "/packages/com.bar.preview", "modbar", "modgus");
}
}
@Test
public void testPreviewModeLinks_disabled() throws IOException {
try (ImageReader reader = ImageReader.open(image, PreviewMode.DISABLED)) {
assertDirContents(reader, "/packages/com.bar", "modbar");
// Missing symbolic link and directory when not in preview mode.
assertAbsent(reader, "/packages/com.bar.preview");
assertAbsent(reader, "/packages/com.bar.preview.stuff");
assertAbsent(reader, "/modules/modbar/com/bar/preview");
assertAbsent(reader, "/modules/modbar/com/bar/preview/stuff");
}
}
@Test
public void testPreviewModeLinks_enabled() throws IOException {
try (ImageReader reader = ImageReader.open(image, PreviewMode.ENABLED)) {
// In preview mode there is a new preview-only module visible.
assertDirContents(reader, "/packages/com.bar", "modbar", "modgus");
// And additional packages are present.
assertDirContents(reader, "/packages/com.bar.preview", "modbar", "modgus");
assertDirContents(reader, "/packages/com.bar.preview.stuff", "modbar");
assertDirContents(reader, "/packages/com.bar.preview.other", "modgus");
// And the preview-only content appears as we expect.
assertDirContents(reader, "/modules/modbar/com/bar", "One.class", "Two.class", "preview");
assertDirContents(reader, "/modules/modbar/com/bar/preview", "stuff");
assertDirContents(reader, "/modules/modbar/com/bar/preview/stuff", "Foo.class", "Bar.class");
// In both modules in which it was added.
assertDirContents(reader, "/modules/modgus/com/bar", "preview");
assertDirContents(reader, "/modules/modgus/com/bar/preview", "other");
assertDirContents(reader, "/modules/modgus/com/bar/preview/other", "Gus.class");
}
}
@ParameterizedTest
@ValueSource(booleans = {false, true})
public void testPreviewEntriesAlwaysHidden(boolean previewMode) throws IOException {
try (ImageReader reader = ImageReader.open(image, previewMode ? PreviewMode.ENABLED : PreviewMode.DISABLED)) {
// The META-INF directory exists, but does not contain the preview directory.
Node dir = assertDir(reader, "/modules/modfoo/META-INF");
assertEquals(0, dir.getChildNames().filter(n -> n.endsWith("/preview")).count());
// Neither the preview directory, nor anything in it, can be looked-up directly.
assertAbsent(reader, "/modules/modfoo/META-INF/preview");
assertAbsent(reader, "/modules/modfoo/META-INF/preview/com/foo");
// HasPreviewVersion.class is a preview class in the test data, and thus appears in
// two places in the jimage). Ensure the preview version is always hidden.
String previewPath = "com/foo/HasPreviewVersion.class";
assertNode(reader, "/modules/modfoo/" + previewPath);
assertAbsent(reader, "/modules/modfoo/META-INF/preview/" + previewPath);
}
}
private static ImageReader.Node assertNode(ImageReader reader, String name) throws IOException {
ImageReader.Node node = reader.findNode(name);
assertNotNull(node, "Could not find node: " + name);
@ -223,9 +360,38 @@ public class ImageReaderTest {
return dir;
}
private static void assertDirContents(ImageReader reader, String name, String... expectedChildNames) throws IOException {
Node dir = assertDir(reader, name);
Set<String> localChildNames = dir.getChildNames()
.peek(s -> assertTrue(s.startsWith(name + "/")))
.map(s -> s.substring(name.length() + 1))
.collect(toSet());
assertEquals(
Set.of(expectedChildNames),
localChildNames,
String.format("Unexpected child names in directory '%s'", name));
}
private static void assertResource(ImageReader reader, String modName, String resPath) throws IOException {
assertTrue(reader.containsResource(modName, resPath), "Resource should exist: " + modName + "/" + resPath);
Node resNode = reader.findResourceNode(modName, resPath);
assertTrue(resNode.isResource(), "Node should be a resource: " + resNode.getName());
String nodeName = "/modules/" + modName + "/" + resPath;
assertEquals(nodeName, resNode.getName());
assertSame(resNode, reader.findNode(nodeName));
}
private static void assertNonPreviewVersion(ImageClassLoader loader, String module, String fqn) throws IOException {
assertEquals("Class: " + fqn, loader.loadAndGetToString(module, fqn));
}
private static void assertPreviewVersion(ImageClassLoader loader, String module, String fqn) throws IOException {
assertEquals("Preview: " + fqn, loader.loadAndGetToString(module, fqn));
}
private static ImageReader.Node assertLink(ImageReader reader, String name) throws IOException {
ImageReader.Node link = assertNode(reader, name);
assertTrue(link.isLink(), "Node was not a symbolic link: " + name);
assertTrue(link.isLink(), "Node should be a symbolic link: " + link.getName());
return link;
}
@ -250,20 +416,23 @@ public class ImageReaderTest {
jar.addEntry("module-info.class", InMemoryJavaCompiler.compile("module-info", moduleInfo));
classes.forEach(fqn -> {
boolean isPreviewEntry = fqn.startsWith("@");
if (isPreviewEntry) {
fqn = fqn.substring(1);
}
int lastDot = fqn.lastIndexOf('.');
String pkg = fqn.substring(0, lastDot);
String cls = fqn.substring(lastDot + 1);
String path = fqn.replace('.', '/') + ".class";
String source = String.format(
"""
package %s;
public class %s {
public String toString() {
return "Class: %s";
return "%s: %s";
}
}
""", pkg, cls, fqn);
""", pkg, cls, isPreviewEntry ? "Preview" : "Class", fqn);
String path = (isPreviewEntry ? "META-INF/preview/" : "") + fqn.replace('.', '/') + ".class";
jar.addEntry(path, InMemoryJavaCompiler.compile(fqn, source));
});
try {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2026, 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
@ -42,6 +42,7 @@ import jdk.internal.jimage.BasicImageReader;
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 @@ public class JImageReadTest {
@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

View File

@ -0,0 +1,238 @@
/*
* Copyright (c) 2025, 2026, 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.ModuleLink;
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 static jdk.internal.jimage.ModuleLink.forEmptyPackage;
import static jdk.internal.jimage.ModuleLink.forPackage;
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 ModuleLink.
* @modules java.base/jdk.internal.jimage
* @run junit/othervm -esa ModuleLinkTest
*/
public final class ModuleLinkTest {
// Copied (not referenced) for testing.
private static final int FLAGS_HAS_PREVIEW_VERSION = 0x1;
private static final int FLAGS_HAS_NORMAL_VERSION = 0x2;
private static final int FLAGS_HAS_CONTENT = 0x4;
@ParameterizedTest
@ValueSource(booleans = {false, true})
public void emptyLinks(boolean isPreview) {
ModuleLink link = forEmptyPackage("module", isPreview);
assertEquals("module", link.name());
assertFalse(link.hasResources());
assertEquals(isPreview, link.hasPreviewVersion());
assertEquals(isPreview, link.isPreviewOnly());
}
@ParameterizedTest
@ValueSource(booleans = {false, true})
public void resourceLinks(boolean isPreview) {
ModuleLink link = forPackage("module", isPreview);
assertEquals("module", link.name());
assertTrue(link.hasResources());
assertEquals(isPreview, link.hasPreviewVersion());
assertEquals(isPreview, link.isPreviewOnly());
}
@ParameterizedTest
@ValueSource(booleans = {false, true})
public void mergedLinks(boolean isPreview) {
ModuleLink emptyLink = forEmptyPackage("module", true);
ModuleLink resourceLink = forPackage("module", isPreview);
ModuleLink merged = emptyLink.merge(resourceLink);
// Merging preserves whether there's content.
assertTrue(merged.hasResources());
// And clears the preview-only status unless it was set in both.
assertEquals(isPreview, merged.isPreviewOnly());
}
@Test
public void writeBuffer() {
List<ModuleLink> links = Arrays.asList(
forEmptyPackage("alpha", true),
forEmptyPackage("beta", false).merge(forEmptyPackage("beta", true)),
forPackage("gamma", false),
forEmptyPackage("zeta", false));
IntBuffer buffer = IntBuffer.allocate(2 * links.size());
ModuleLink.write(links, buffer, fakeEncoder());
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,
() -> ModuleLink.write(List.of(), buffer, null));
assertTrue(err.getMessage().contains("non-empty"));
}
@Test
public void writeBuffer_badCapacity() {
List<ModuleLink> links = Arrays.asList(
forPackage("first", false),
forEmptyPackage("alpha", false));
IntBuffer buffer = IntBuffer.allocate(10);
var err = assertThrows(
IllegalArgumentException.class,
() -> ModuleLink.write(links, buffer, null));
assertTrue(err.getMessage().contains("buffer capacity"));
}
@Test
public void writeBuffer_multiplePackagesWithResources() {
// Only one module link (at most) can have resources.
List<ModuleLink> links = Arrays.asList(
forPackage("alpha", false),
forPackage("beta", false));
IntBuffer buffer = IntBuffer.allocate(2 * links.size());
var err = assertThrows(
IllegalArgumentException.class,
() -> ModuleLink.write(links, buffer, null));
assertTrue(err.getMessage().contains("resources"));
}
@Test
public void writeBuffer_badOrdering() {
// Badly ordered because preview references should come first.
List<ModuleLink> links = Arrays.asList(
forEmptyPackage("alpha", false),
forEmptyPackage("beta", true));
IntBuffer buffer = IntBuffer.allocate(2 * links.size());
var err = assertThrows(
IllegalArgumentException.class,
() -> ModuleLink.write(links, buffer, null));
assertTrue(err.getMessage().contains("strictly ordered"));
}
@Test
public void writeBuffer_duplicateLink() {
// Technically distinct, and correctly sorted, but with duplicate names.
List<ModuleLink> links = Arrays.asList(
forEmptyPackage("duplicate", true),
forEmptyPackage("duplicate", false));
IntBuffer buffer = IntBuffer.allocate(2 * links.size());
var err = assertThrows(
IllegalArgumentException.class,
() -> ModuleLink.write(links, 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<Integer> normalOffsets = asList(ModuleLink.readNameOffsets(buffer, true, false));
List<Integer> previewOffsets = asList(ModuleLink.readNameOffsets(buffer, false, true));
List<Integer> allOffsets = asList(ModuleLink.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,
() -> ModuleLink.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,
() -> ModuleLink.readNameOffsets(buffer, false, false));
assertTrue(err.getMessage().contains("flags"));
}
@Test
public void sortOrder_previewFirst() {
List<ModuleLink> links = Arrays.asList(
forEmptyPackage("normal.beta", false),
forPackage("preview.beta", true),
forEmptyPackage("preview.alpha", true),
forEmptyPackage("normal.alpha", false));
links.sort(Comparator.naturalOrder());
// Non-empty first with remaining sorted by name.
assertEquals(
List.of("preview.alpha", "preview.beta", "normal.alpha", "normal.beta"),
links.stream().map(ModuleLink::name).toList());
}
private static <T> List<T> asList(Iterator<T> src) {
List<T> list = new ArrayList<>();
src.forEachRemaining(list::add);
return list;
}
// Encodes strings sequentially starting from index 100.
private static Function<String, Integer> fakeEncoder() {
List<String> 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;
}
};
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, 2026, 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
@ -22,6 +22,7 @@
*/
import jdk.internal.jimage.ImageReader;
import jdk.internal.jimage.PreviewMode;
import java.nio.file.Files;
import java.nio.file.Path;
@ -54,7 +55,7 @@ public class ImageReaderDuplicateChildNodesTest {
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.DISABLED)) {
// find the child node/resource first
final ImageReader.Node integerNode = reader.findNode(integerResource);
if (integerNode == null) {

View File

@ -260,7 +260,8 @@ public abstract class VerifyJimage implements Runnable {
*/
private boolean isJimageOnly(String entryName) {
return entryName.startsWith("/java.base/jdk/internal/module/SystemModules$")
|| entryName.startsWith("/java.base/java/lang/invoke/BoundMethodHandle$Species_");
|| entryName.startsWith("/java.base/java/lang/invoke/BoundMethodHandle$Species_")
|| entryName.startsWith("/jdk.jlink/jdk/tools/jlink/internal/runtimelink/");
}
private String getEntryName(Path path) {

View File

@ -0,0 +1,193 @@
/*
* Copyright (c) 2026, 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.test.lib.compiler.InMemoryJavaCompiler;
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import jdk.test.lib.util.JarBuilder;
import jdk.tools.jlink.internal.LinkableRuntimeImage;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import tests.Helper;
import tests.JImageGenerator;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.spi.ToolProvider;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*
* @test
* @summary Tests preview mode support in JLink.
* @library /test/jdk/tools/lib
* /test/lib
* @build jdk.test.lib.process.ProcessTools
* tests.*
* @modules jdk.jlink/jdk.tools.jimage
* jdk.jlink/jdk.tools.jlink.internal
* java.base/jdk.internal.jimage
* @run junit/othervm JLinkPreviewTest
*/
public class JLinkPreviewTest {
private static final String TEST_MODULE = "java.test";
private static final String TEST_PACKAGE = "test";
private static final String TEST_CLASS = "InjectedTestClass";
private static final int NORMAL_EXIT_VALUE = 23;
private static final int PREVIEW_EXIT_VALUE = 42;
private static final ToolProvider JLINK_TOOL = ToolProvider.findFirst("jlink")
.orElseThrow(() -> new RuntimeException("jlink tool not found"));
private static Path customJreRoot;
@BeforeAll
static void buildCustomBootImage(@TempDir Path tmp) throws Exception {
Path jreRoot = tmp.resolve("testjdk");
if (JLINK_TOOL.run(System.out, System.err,
"--add-modules", "java.base",
"--add-modules", "jdk.zipfs",
"--output", jreRoot.toString()) != 0) {
throw new RuntimeException("failed to create small boot image");
}
Path jimage = jreRoot.resolve("lib", "modules");
Helper helper = getHelper();
// Compile into the helper's jar directory so jlink will include it.
compileTestModule(helper.getJarDir());
Path customJimage = buildJimage(helper);
Files.copy(customJimage, jimage, REPLACE_EXISTING);
customJreRoot = jreRoot;
}
@Test
public void nonPreviewMode() throws Exception {
runTestClass(false, NORMAL_EXIT_VALUE, TEST_CLASS + ": NORMAL");
}
@Test
public void previewMode() throws Exception {
runTestClass(true, PREVIEW_EXIT_VALUE, TEST_CLASS + ": PREVIEW");
}
@Test
public void ensureJimageContent() {
Path jimage = customJreRoot.resolve("lib", "modules");
// The jimage tool isn't present in the custom JRE, but should
// have the same version by virtue of coming from the test JVM.
StringWriter buffer = new StringWriter();
assertEquals(0, jdk.tools.jimage.Main.run(new String[] { "list", jimage.toString() }, new PrintWriter(buffer)));
List<String> outLines = buffer.toString().lines().map(String::strip).toList();
String pkgPath = getPackagePath(TEST_PACKAGE + "." + TEST_CLASS);
assertTrue(outLines.contains("Module: " + TEST_MODULE));
assertTrue(outLines.contains(pkgPath));
assertTrue(outLines.contains("META-INF/preview/" + pkgPath));
}
/// Returns the helper for building JAR and jimage files.
private static Helper getHelper() {
Helper helper;
try {
boolean isLinkableRuntime = LinkableRuntimeImage.isLinkableRuntime();
helper = Helper.newHelper(isLinkableRuntime);
} catch (IOException e) {
throw new RuntimeException(e);
}
Assumptions.assumeTrue(helper != null, "Cannot create test helper, skipping test!");
return helper;
}
/// Builds a jimage file with the specified class entries. The classes in
/// the built image can be loaded and executed to return their names via
/// `toString()` to confirm the correct bytes were returned.
private static Path buildJimage(Helper helper) {
Path outDir = helper.createNewImageDir("test");
// The default module path contains the directory we compiled the jars into.
JImageGenerator.JLinkTask jlink = JImageGenerator.getJLinkTask()
.modulePath(helper.defaultModulePath())
.output(outDir);
jlink.addMods(TEST_MODULE);
return jlink.call().assertSuccess().resolve("lib", "modules");
}
/// Compiles a test module containing test classes into a single Jar.
/// The test class can be instantiated and have their {@code toString()}
/// method called to return a status string for testing.
private static void compileTestModule(Path jarDir) throws IOException {
JarBuilder jar = new JarBuilder(jarDir.resolve(TEST_MODULE + ".jar").toString());
String moduleInfo = "open module " + TEST_MODULE + " {}";
jar.addEntry("module-info.class", InMemoryJavaCompiler.compile("module-info", moduleInfo));
compileTestClass(jar, false);
compileTestClass(jar, true);
jar.build();
}
/// Compiles a test class into a given single Jar.
private static void compileTestClass(JarBuilder jar, boolean isPreview) {
String fqn = TEST_PACKAGE + "." + TEST_CLASS;
String msg = isPreview ? "PREVIEW" : "NORMAL";
int exit = isPreview ? PREVIEW_EXIT_VALUE : NORMAL_EXIT_VALUE;
String testSrc = String.format(
"""
package %1$s;
public class %2$s {
public static void main(String[] args) {
System.out.println("%2$s: %3$s");
System.out.flush();
System.exit(%4$d);
}
}
""", TEST_PACKAGE, TEST_CLASS, msg, exit);
String pkgPath = getPackagePath(fqn);
String path = (isPreview ? "META-INF/preview/" : "") + pkgPath;
jar.addEntry(path, InMemoryJavaCompiler.compile(fqn, testSrc));
}
private static void runTestClass(boolean isPreviewMode, int expectedExitValue, String expectedMessage) throws Exception {
List<String> args = new ArrayList<>();
args.add(customJreRoot.resolve("bin", "java").toString());
if (isPreviewMode) {
args.add("--enable-preview");
}
args.add("-m");
args.add(TEST_MODULE + "/" + TEST_PACKAGE + "." + TEST_CLASS);
ProcessBuilder cmd = new ProcessBuilder(args);
OutputAnalyzer result = ProcessTools.executeCommand(cmd);
assertEquals(expectedExitValue, result.getExitValue());
assertEquals(expectedMessage + System.lineSeparator(), result.getStdout());
}
private static String getPackagePath(String fqn) {
return fqn.replace('.', '/') + ".class";
}
}

View File

@ -24,6 +24,7 @@ package org.openjdk.bench.jdk.internal.jrtfs;
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;
@ -93,7 +94,7 @@ public class ImageReaderBenchmark {
@Setup(Level.Trial)
public void setUp() throws IOException {
super.setUp();
reader = ImageReader.open(copiedImageFile, byteOrder);
reader = ImageReader.open(copiedImageFile, byteOrder, PreviewMode.DISABLED);
}
@TearDown(Level.Trial)
@ -122,7 +123,7 @@ public class ImageReaderBenchmark {
@Setup(Level.Iteration)
public void setup() throws IOException {
super.setUp();
reader = ImageReader.open(copiedImageFile, byteOrder);
reader = ImageReader.open(copiedImageFile, byteOrder, PreviewMode.DISABLED);
}
@TearDown(Level.Iteration)
@ -149,7 +150,7 @@ public class ImageReaderBenchmark {
@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, state.byteOrder, PreviewMode.DISABLED)) {
state.count = countAllNodes(reader, reader.findNode("/"));
}
}
@ -173,7 +174,7 @@ public class ImageReaderBenchmark {
@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, state.byteOrder, 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 +211,7 @@ public class ImageReaderBenchmark {
// 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<String> missing = new ArrayList<>(errors);
try (var reader = ImageReader.open(state.copiedImageFile, state.byteOrder)) {
try (var reader = ImageReader.open(state.copiedImageFile, state.byteOrder, PreviewMode.DISABLED)) {
for (String path : INIT_CLASSES) {
if (reader.findNode(path) == null) {
missing.add(path);