mirror of
https://github.com/openjdk/jdk.git
synced 2026-05-16 08:29:34 +00:00
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:
parent
bbd5f6d877
commit
4edfc387f1
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
290
src/java.base/share/classes/jdk/internal/jimage/ModuleLink.java
Normal file
290
src/java.base/share/classes/jdk/internal/jimage/ModuleLink.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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() {}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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 + "]");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
135
test/jdk/jdk/internal/jimage/ImageLocationTest.java
Normal file
135
test/jdk/jdk/internal/jimage/ImageLocationTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
238
test/jdk/jdk/internal/jimage/ModuleLinkTest.java
Normal file
238
test/jdk/jdk/internal/jimage/ModuleLinkTest.java
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
193
test/jdk/tools/jlink/JLinkPreviewTest.java
Normal file
193
test/jdk/tools/jlink/JLinkPreviewTest.java
Normal 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";
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user