From 4edfc387f160dfeb8a67408b1bd98ea45f51d36c Mon Sep 17 00:00:00 2001 From: Alan Bateman Date: Tue, 12 May 2026 10:09:28 +0000 Subject: [PATCH] 8377070: Update jimage format to support classes compiled with preview feature enabled Co-authored-by: David Beaumont Reviewed-by: jpai, coleenp, sgehwolf --- src/hotspot/share/classfile/classLoader.cpp | 177 +++--- src/hotspot/share/classfile/classLoader.hpp | 27 +- src/hotspot/share/runtime/arguments.cpp | 4 + .../jdk/internal/jimage/BasicImageReader.java | 53 ++ .../jdk/internal/jimage/ImageHeader.java | 21 +- .../jdk/internal/jimage/ImageLocation.java | 235 ++++++-- .../jdk/internal/jimage/ImageReader.java | 537 ++++++++++++------ .../jdk/internal/jimage/ImageStrings.java | 19 +- .../jdk/internal/jimage/ModuleLink.java | 290 ++++++++++ .../jdk/internal/jimage/PreviewMode.java | 86 +++ .../jdk/internal/jimage/ResourceEntries.java | 73 +++ ...derFactory.java => SystemImageReader.java} | 69 +-- .../jdk/internal/jrtfs/JrtFileSystem.java | 6 +- .../jdk/internal/jrtfs/SystemImage.java | 69 ++- .../internal/module/SystemModuleFinders.java | 4 +- .../jrt/JavaRuntimeURLConnection.java | 13 +- .../share/native/libjimage/imageFile.cpp | 6 +- .../share/native/libjimage/imageFile.hpp | 30 +- .../share/native/libjimage/jimage.cpp | 109 +++- .../share/native/libjimage/jimage.hpp | 9 +- .../classes/jdk/tools/jimage/JImageTask.java | 19 +- .../jdk/tools/jlink/internal/Archive.java | 21 +- .../jlink/internal/BasicImageWriter.java | 48 +- .../jlink/internal/ImageFileCreator.java | 126 ++-- .../jlink/internal/ImageLocationWriter.java | 25 +- .../jlink/internal/ImageResourcesTree.java | 386 +++++++------ .../jlink/internal/ImageStringsWriter.java | 25 +- .../jdk/tools/jlink/internal/JRTArchive.java | 220 ++++--- .../jdk/tools/jlink/internal/JlinkTask.java | 88 +-- .../jlink/internal/ResourcePoolManager.java | 61 +- .../internal/jimage/ImageLocationTest.java | 135 +++++ .../jdk/internal/jimage/ImageReaderTest.java | 259 +++++++-- .../jdk/internal/jimage/JImageReadTest.java | 9 +- .../jdk/internal/jimage/ModuleLinkTest.java | 238 ++++++++ .../ImageReaderDuplicateChildNodesTest.java | 5 +- test/jdk/tools/jimage/VerifyJimage.java | 3 +- test/jdk/tools/jlink/JLinkPreviewTest.java | 193 +++++++ .../internal/jrtfs/ImageReaderBenchmark.java | 11 +- 38 files changed, 2719 insertions(+), 990 deletions(-) create mode 100644 src/java.base/share/classes/jdk/internal/jimage/ModuleLink.java create mode 100644 src/java.base/share/classes/jdk/internal/jimage/PreviewMode.java create mode 100644 src/java.base/share/classes/jdk/internal/jimage/ResourceEntries.java rename src/java.base/share/classes/jdk/internal/jimage/{ImageReaderFactory.java => SystemImageReader.java} (57%) create mode 100644 test/jdk/jdk/internal/jimage/ImageLocationTest.java create mode 100644 test/jdk/jdk/internal/jimage/ModuleLinkTest.java create mode 100644 test/jdk/tools/jlink/JLinkPreviewTest.java diff --git a/src/hotspot/share/classfile/classLoader.cpp b/src/hotspot/share/classfile/classLoader.cpp index 6d25e460688..bf00185ffa9 100644 --- a/src/hotspot/share/classfile/classLoader.cpp +++ b/src/hotspot/share/classfile/classLoader.cpp @@ -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* ClassLoader::_patch_mod_entries = nullptr; GrowableArray* 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(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() { diff --git a/src/hotspot/share/classfile/classLoader.hpp b/src/hotspot/share/classfile/classLoader.hpp index a935d3027ac..ff7e8999688 100644 --- a/src/hotspot/share/classfile/classLoader.hpp +++ b/src/hotspot/share/classfile/classLoader.hpp @@ -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* _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* _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 diff --git a/src/hotspot/share/runtime/arguments.cpp b/src/hotspot/share/runtime/arguments.cpp index d16aded7309..341e0b80a5f 100644 --- a/src/hotspot/share/runtime/arguments.cpp +++ b/src/hotspot/share/runtime/arguments.cpp @@ -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; } diff --git a/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java index 1c565a52c0f..20d3b8837fe 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java @@ -37,8 +37,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. + * + *

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

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 getEntryNames(String module) { + if (module.isEmpty() || module.equals("modules") || module.equals("packages")) { + throw new IllegalArgumentException("Invalid module name: " + module); + } + return IntStream.range(0, offsets.capacity()) + .map(offsets::get) + .filter(offset -> offset != 0) + // Reusing a location instance or getting the module + // offset directly would save a lot of allocations here. + .mapToObj(offset -> ImageLocation.readFrom(BasicImageReader.this, offset)) + // Reverse lookup of module offset would be faster here. + .filter(loc -> module.equals(loc.getModule())) + .map(ImageLocation::getFullName); + } + + private ImageLocation getResourceLocation(String name) { + if (!name.startsWith("/modules/") && !name.startsWith("/packages/")) { + ImageLocation location = BasicImageReader.this.findLocation(name); + if (location != null) { + return location; + } + } + throw new NoSuchElementException("No such resource entry: " + name); + } + + @Override + public long getSize(String name) { + return getResourceLocation(name).getUncompressedSize(); + } + + @Override + public byte[] getBytes(String name) { + return BasicImageReader.this.getResource(getResourceLocation(name)); + } + }; + } + ImageLocation getLocation(int offset) { return ImageLocation.readFrom(this, offset); } diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageHeader.java b/src/java.base/share/classes/jdk/internal/jimage/ImageHeader.java index f63665119e2..c128aa9736a 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageHeader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageHeader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 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. + * + *

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

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

    + *
  • src/java.base/share/native/libjimage/imageFile.hpp + *
+ * + *

Version history: + *

    + *
  • {@code 1.0}: Original version. + *
  • {@code 1.1}: Support preview mode with new flags. + *
+ * * @implNote This class needs to maintain JDK 8 source compatibility. * * It is used internally in the JDK to implement jimage/jrtfs access, @@ -39,7 +56,7 @@ 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; diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageLocation.java b/src/java.base/share/classes/jdk/internal/jimage/ImageLocation.java index f31c7291927..0822b17f0bb 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageLocation.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageLocation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 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. + * + *

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

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

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

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

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

This flag is mutually exclusive with {@link #FLAGS_HAS_PREVIEW_VERSION} + * and need not imply that {@link #FLAGS_IS_PREVIEW_VERSION} is set (i.e. + * for {@code /packages/xxx} directories). + */ + 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). + * + *

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. + * + *

Based on the entry name, the flags are: + *

    + *
  • {@code "[/modules]//"} normal resource or directory:
    + * Zero, or {@code FLAGS_HAS_PREVIEW_VERSION} if a preview entry exists. + *
  • {@code "[/modules]//META-INF/preview/"} preview + * resource or directory:
    + * {@code FLAGS_IS_PREVIEW_VERSION}, and additionally {@code + * FLAGS_IS_PREVIEW_ONLY} if no normal version of the resource or + * directory exists. + *
  • In all other cases, returned flags are zero (note that {@code + * "/packages/xxx"} entries may have flags, but these are calculated + * elsewhere). + *
+ * + * @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 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. + * + *

Based on the module links, the flags are: + *

    + *
  • {@code FLAGS_HAS_PREVIEW_VERSION} if any referenced + * package has a preview version. + *
  • {@code FLAGS_IS_PREVIEW_ONLY} if all referenced packages + * are preview only. + *
+ * + * @return package flags for {@code "/packages/xxx"} directory entries. + */ + public static int getPackageFlags(List 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 // and references a resource. + return LocationType.RESOURCE; + } + } + + private boolean isRootDir() { + if (getModuleOffset() == 0 && getParentOffset() == 0) { + String name = getFullName(); + return name.equals(MODULES_PREFIX) || name.equals(PACKAGES_PREFIX); + } + return false; + } + + @Override + public String toString() { + // Cannot use String.format() (too early in startup for locale code). + return "ImageLocation[name='" + getFullName() + "', type=" + getType() + ", flags=" + getFlags() + "]"; + } + static ImageLocation readFrom(BasicImageReader reader, int offset) { Objects.requireNonNull(reader); long[] attributes = reader.getAttributes(offset); diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java index 4c358820166..2cf28b835ce 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java @@ -24,6 +24,8 @@ */ package jdk.internal.jimage; +import jdk.internal.jimage.ImageLocation.LocationType; + import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -34,16 +36,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 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 OPEN_FILES = new HashMap<>(); + // List of openers for this shared image. private final Set 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 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 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 /}). We can't just pass in the - * {@code /} string here because that has a module offset of zero. + * Process {@code "/packages/xxx"} entries to build the child nodes for the + * root {@code "/packages"} node. Preview-only entries will be skipped if + * {@code previewMode == false}. + * + *

If {@code previewMode == true}, this method also populates the {@link + * #previewDirectoriesToMerge} map with any preview-only nodes, to be merged + * into directories as they are completed. It also caches preview resources + * and preview-only directories for direct lookup. */ - private int getModuleOffset(String path) { - ImageLocation location = findLocation(path); - assert location != null : "Cannot find expected jimage location: " + path; - int offset = location.getModuleOffset(); - assert offset != 0 : "Invalid module offset for jimage location: " + path; - return offset; + private ArrayList processPackagesDirectory(boolean previewMode) { + ImageLocation pkgRoot = findLocation(PACKAGES_PREFIX); + assert pkgRoot != null : "Invalid jimage file"; + IntBuffer offsets = getOffsetBuffer(pkgRoot); + ArrayList pkgDirs = new ArrayList<>(offsets.capacity()); + // Package path to module map, sorted in reverse order so that + // longer child paths get processed first. + Map> previewPackagesToModules = + new TreeMap<>(Comparator.reverseOrder()); + for (int i = 0; i < offsets.capacity(); i++) { + ImageLocation pkgDir = getLocation(offsets.get(i)); + int flags = pkgDir.getFlags(); + // A package subdirectory is "preview only" if all the modules + // it references have that package marked as preview only. + // Skipping these entries avoids empty package subdirectories. + if (previewMode || !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 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 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 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 { * *

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 { *

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. - * - *

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". + * + *

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. - * - *

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

The given directory can be the top level {@code /packages} directory, - * so it is NOT safe to use {@code isPackagesSubdirectory(loc)} here. - */ - private Directory completePackageDirectory(Directory dir, ImageLocation loc) { + /** Completes a package directory by setting the list of child nodes. */ + private void completePackageSubdirectory(Directory dir, ImageLocation loc) { assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir; - // The only directories in the "/packages" namespace are "/packages" or - // "/packages/". However, unlike "/modules" directories, the - // location offsets mean different things. - List children; - if (dir.getName().equals(PACKAGES_ROOT)) { - // Top-level directory just contains a list of subdirectories. - children = createChildNodes(loc, c -> nodes.computeIfAbsent(c.getFullName(), this::newDirectory)); - } else { - // A package directory's content is array of offset PAIRS in the - // Strings table, but we only need the 2nd value of each pair. - IntBuffer intBuffer = getOffsetBuffer(loc); - int offsetCount = intBuffer.capacity(); - assert (offsetCount & 0x1) == 0 : "Offset count must be even: " + offsetCount; - children = new ArrayList<>(offsetCount / 2); - // Iterate the 2nd offset in each pair (odd indices). - for (int i = 1; i < offsetCount; i += 2) { - String moduleName = getString(intBuffer.get(i)); - children.add(nodes.computeIfAbsent( - dir.getName() + "/" + moduleName, - n -> newLinkNode(n, MODULES_ROOT + "/" + moduleName))); + assert !dir.isCompleted() : "Directory already completed: " + dir; + assert loc.getType() == PACKAGES_DIR : "Invalid location type: " + loc.getType(); + + // In non-preview mode we might skip a very small number of preview-only + // entries, but it's not worth "right-sizing" the array for that. + IntBuffer offsets = getOffsetBuffer(loc); + List children = new ArrayList<>(offsets.capacity() / 2); + 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. + * + *

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 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. * - *

Note: This cannot be used for package subdirectories as they have - * child offsets stored differently to other directories. + *

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 createChildNodes(ImageLocation loc, Function newChildFn) { + private List createChildNodes(ImageLocation loc, int extraNodesCount, Function getChildFn) { + LocationType type = loc.getType(); + assert type == MODULES_DIR || type == MODULES_ROOT : "Invalid location type: " + loc; IntBuffer offsets = getOffsetBuffer(loc); int childCount = offsets.capacity(); - List children = new ArrayList<>(childCount); + List children = new ArrayList<>(childCount + extraNodesCount); for (int i = 0; i < childCount; i++) { - children.add(newChildFn.apply(getLocation(offsets.get(i)))); + Node childNode = getChildFn.apply(getLocation(offsets.get(i))); + if (childNode != null) { + children.add(childNode); + } } return children; } /** Helper to extract the integer offset buffer from a directory location. */ private IntBuffer getOffsetBuffer(ImageLocation dir) { - assert !isResource(dir) : "Not a directory: " + dir.getFullName(); + assert dir.getType() != RESOURCE : "Not a directory: " + dir.getFullName(); byte[] offsets = getResource(dir); ByteBuffer buffer = ByteBuffer.wrap(offsets); buffer.order(getByteOrder()); return buffer.asIntBuffer(); } - /** - * Efficiently determines if an image location is a resource. - * - *

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

In jimage, every {@code ImageLocation} under {@code /modules/} is a - * directory and has the same value for {@code getModule()}, and {@code - * getModuleOffset()}. - */ - private boolean isModulesSubdirectory(ImageLocation loc) { - return loc.getModuleOffset() == modulesStringOffset; - } - /** * Creates an "incomplete" directory node with no child nodes set. * Directories need to be "completed" before they are returned by @@ -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 children) { + private void setChildren(List children) { assert this.children == null : this + ": Cannot set child nodes twice!"; this.children = Collections.unmodifiableList(children); } diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageStrings.java b/src/java.base/share/classes/jdk/internal/jimage/ImageStrings.java index eea62e444de..9cd1e662d64 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageStrings.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageStrings.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 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); diff --git a/src/java.base/share/classes/jdk/internal/jimage/ModuleLink.java b/src/java.base/share/classes/jdk/internal/jimage/ModuleLink.java new file mode 100644 index 00000000000..57224906179 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/jimage/ModuleLink.java @@ -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). + * + *

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. + * + *

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 { + // 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 PREVIEW_FIRST = + Comparator.comparing(ModuleLink::hasPreviewVersion).reversed() + .thenComparing(ModuleLink::name); + + /** + * Returns a link for non-empty packages (those with resources) in a + * given module. + * + *

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. + * + *

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. + * + *

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. + * + *

Package subdirectories store their entries using pairs of integers in + * an interleaved buffer: + *

+     *     ...
+     *     [ entry-N flags ]
+     *     [ entry-N name offset ]
+     *     [ entry-(N+1) flags ]
+     *     [ entry-(N+1) name offset ]
+     *     ...
+     * 
+ * + *

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 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() { + 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. + * + *

Entries are written in order, taking two integer slots per entry as + * {@code [, ]}. + * + * @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 links, IntBuffer buffer, Function 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)); + } + } +} diff --git a/src/java.base/share/classes/jdk/internal/jimage/PreviewMode.java b/src/java.base/share/classes/jdk/internal/jimage/PreviewMode.java new file mode 100644 index 00000000000..dd273494b15 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/jimage/PreviewMode.java @@ -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); + } + } +} diff --git a/src/java.base/share/classes/jdk/internal/jimage/ResourceEntries.java b/src/java.base/share/classes/jdk/internal/jimage/ResourceEntries.java new file mode 100644 index 00000000000..18b3675d399 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/jimage/ResourceEntries.java @@ -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. + * + *

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. + * + *

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

It disallows access to resource directories (i.e. {@code "/modules/..."}) + * or packages entries (i.e. {@code "/packages/..."}). + * + *

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 "//..."}). + */ + Stream getEntryNames(String module); + + /** + * Returns the (uncompressed) size of a resource given its jimage name. + * + * @throws java.util.NoSuchElementException if the resource does not exist. + */ + long getSize(String name); + + /** + * Returns a copy of a resource's content given its jimage name. + * + * @throws java.util.NoSuchElementException if the resource does not exist. + */ + byte[] getBytes(String name); +} diff --git a/src/java.base/share/classes/jdk/internal/jimage/ImageReaderFactory.java b/src/java.base/share/classes/jdk/internal/jimage/SystemImageReader.java similarity index 57% rename from src/java.base/share/classes/jdk/internal/jimage/ImageReaderFactory.java rename to src/java.base/share/classes/jdk/internal/jimage/SystemImageReader.java index 2ecec20d6f9..87f301c8ba8 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageReaderFactory.java +++ b/src/java.base/share/classes/jdk/internal/jimage/SystemImageReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 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 readers = new ConcurrentHashMap<>(); - - /** - * Returns an {@code ImageReader} to read from the given image file - */ - public static ImageReader get(Path jimage) throws IOException { - Objects.requireNonNull(jimage); try { - return readers.computeIfAbsent(jimage, OPENER); - } catch (UncheckedIOException io) { - throw io.getCause(); + SYSTEM_IMAGE_READER = ImageReader.open(fs.getPath(javaHome, "lib", "modules"), PreviewMode.FOR_RUNTIME); + } catch (IOException e) { + throw new ExceptionInInitializerError(e); } } - private static Function OPENER = new Function() { - public ImageReader apply(Path path) { - try { - return ImageReader.open(path); - } catch (IOException io) { - throw new UncheckedIOException(io); - } - } - }; - /** - * Returns the {@code ImageReader} to read the image file in this - * run-time image. + * Returns the singleton {@code ImageReader} to read the image file in this + * run-time image. The returned instance must not be closed. * * @throws UncheckedIOException if an I/O error occurs */ - public static ImageReader getImageReader() { - try { - return get(BOOT_MODULES_JIMAGE); - } catch (IOException ioe) { - throw new UncheckedIOException(ioe); - } + public static ImageReader get() { + return SYSTEM_IMAGE_READER; } + + /** + * Returns the "raw" API for accessing underlying jimage resource entries. + * + *

This is only meaningful for use by code dealing directly with jimage + * files, and cannot be used to reliably lookup resources used at runtime. + */ + public static ResourceEntries getResourceEntries() { + return get().getResourceEntries(); + } + + private SystemImageReader() {} } diff --git a/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java b/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java index c405801506f..4cd47413bd0 100644 --- a/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java +++ b/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java @@ -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; } diff --git a/src/java.base/share/classes/jdk/internal/jrtfs/SystemImage.java b/src/java.base/share/classes/jdk/internal/jrtfs/SystemImage.java index b38e953a5f9..560e4942a17 100644 --- a/src/java.base/share/classes/jdk/internal/jrtfs/SystemImage.java +++ b/src/java.base/share/classes/jdk/internal/jrtfs/SystemImage.java @@ -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; diff --git a/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java b/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java index afebb89168c..565da646e28 100644 --- a/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java +++ b/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java @@ -54,7 +54,7 @@ import java.util.stream.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; } diff --git a/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java index 71080950b80..d994f2d3842 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/jrt/JavaRuntimeURLConnection.java @@ -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); diff --git a/src/java.base/share/native/libjimage/imageFile.cpp b/src/java.base/share/native/libjimage/imageFile.cpp index e2479ba2c9e..b5e65da59bc 100644 --- a/src/java.base/share/native/libjimage/imageFile.cpp +++ b/src/java.base/share/native/libjimage/imageFile.cpp @@ -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); diff --git a/src/java.base/share/native/libjimage/imageFile.hpp b/src/java.base/share/native/libjimage/imageFile.hpp index a4c8d159efa..ccc0e0b1ad1 100644 --- a/src/java.base/share/native/libjimage/imageFile.hpp +++ b/src/java.base/share/native/libjimage/imageFile.hpp @@ -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. diff --git a/src/java.base/share/native/libjimage/jimage.cpp b/src/java.base/share/native/libjimage/jimage.cpp index 91a86f992e6..d2830bd3f5c 100644 --- a/src/java.base/share/native/libjimage/jimage.cpp +++ b/src/java.base/share/native/libjimage/jimage.cpp @@ -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); } /* diff --git a/src/java.base/share/native/libjimage/jimage.hpp b/src/java.base/share/native/libjimage/jimage.hpp index a514e737b49..c340b8b7c18 100644 --- a/src/java.base/share/native/libjimage/jimage.hpp +++ b/src/java.base/share/native/libjimage/jimage.hpp @@ -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); diff --git a/src/jdk.jlink/share/classes/jdk/tools/jimage/JImageTask.java b/src/jdk.jlink/share/classes/jdk/tools/jimage/JImageTask.java index 3f324ba1364..402f6047553 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jimage/JImageTask.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jimage/JImageTask.java @@ -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) : - ""; - + String newModule = location.getModule(); + if (newModule.isEmpty()) { + newModule = ""; + } if (!oldModule.equals(newModule)) { moduleAction.apply(reader, oldModule, newModule); oldModule = newModule; } } - - ImageLocation location = reader.findLocation(name); resourceAction.apply(reader, name, location); } } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Archive.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Archive.java index c56346b6994..1cd8f5993d7 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Archive.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/Archive.java @@ -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; } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/BasicImageWriter.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/BasicImageWriter.java index ca576227692..67895dd1450 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/BasicImageWriter.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/BasicImageWriter.java @@ -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 input; - private ImageStream headerStream; - private ImageStream redirectStream; - private ImageStream locationOffsetStream; - private ImageStream locationStream; - private ImageStream allIndexStream; - - public BasicImageWriter() { - this(ByteOrder.nativeOrder()); - } + private final List 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 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]; - } } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageFileCreator.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageFileCreator.java index 9e05fe31aa9..b29a9de5212 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageFileCreator.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageFileCreator.java @@ -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 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 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 ""; - } - } } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageLocationWriter.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageLocationWriter.java index f2c7f102027..18ee9c88103 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageLocationWriter.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageLocationWriter.java @@ -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 diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageResourcesTree.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageResourcesTree.java index 8a4288f0cfd..2f8143820ca 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageResourcesTree.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageResourcesTree.java @@ -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 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/"}. + * + *

While package paths can exist within many modules, for each package + * there is at most one module in which that package has resources. + * + *

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. + * + *

When processing module links in non-preview mode, entries marked + * as {@link ModuleLink#isPreviewOnly() preview-only} must be ignored. + * + *

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 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 references = new TreeMap<>(); - - PackageNode(String name, Node parent) { + PackageNode(String name, List 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 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 directAccess = new HashMap<>(); private final List paths; private final Node root; - private Node modules; - private Node packages; + private Node packagesRoot; - private Tree(List paths) { - this.paths = paths; + // Visible for testing only. + Tree(List 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> moduleToPackage = new TreeMap<>(); - Map> packageToModule = new TreeMap<>(); - - for (String p : paths) { - if (!p.startsWith("/")) { - continue; - } - String[] split = p.split("/"); - // minimum length is 3 items: // - 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> 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 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 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> 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> 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 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> 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 links = ((PackageNode) current).getModuleLinks(); + // "/packages/" 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 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 outLocations) { if (current instanceof PackageNode) { - // /packages/ - 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/" entries have 8-byte entries (flags+offset). + List 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()) { diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageStringsWriter.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageStringsWriter.java index 7ba9b7db72e..18794d46b94 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageStringsWriter.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageStringsWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 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 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 + "]"); } } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java index aac220e5b94..284f1cf6c77 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2024, Red Hat, Inc. + * Copyright (c) 2025, 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 files = new ArrayList<>(); // Files not part of the lib/modules image of the JDK install. @@ -99,12 +100,11 @@ public class JRTArchive implements Archive { Set upgradeableFiles) { this.module = module; this.path = path; - this.ref = ModuleFinder.ofSystem() - .find(module) - .orElseThrow(() -> - new IllegalArgumentException( - "Module " + module + - " not part of the JDK install")); + ModuleFinder.ofSystem() + .find(module) + .orElseThrow(() -> new IllegalArgumentException( + "Module " + module + " not part of the JDK install")); + this.imageResources = SystemImageReader.getResourceEntries(); this.errorOnModifiedFile = errorOnModifiedFile; this.otherRes = readModuleResourceFile(module); this.resDiff = Objects.requireNonNull(perModDiff).stream() @@ -159,52 +159,35 @@ public 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: ||| * - * 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()); } - }; } } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java index 3baae08eed6..45800be5272 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JlinkTask.java @@ -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 archives, - Platform targetPlatform, - Path packagedModulesPath, - boolean generateRuntimeImage) implements ImageProvider { + private record ImageHelper(Set archives, + Platform targetPlatform, + Path packagedModulesPath, + boolean generateRuntimeImage) + implements ImageProvider, AutoCloseable { @Override public ExecutableImage retrieve(ImagePluginStack stack) throws IOException { ExecutableImage image = ImageFileCreator.create(archives, @@ -1073,5 +1077,25 @@ public class JlinkTask { } return image; } + + @Override + public void close() throws IOException { + List thrown = null; + for (Archive archive : archives) { + try { + archive.close(); + } catch (IOException ex) { + if (thrown == null) { + thrown = new ArrayList<>(); + } + thrown.add(ex); + } + } + if (thrown != null) { + IOException ex = new IOException("Archives could not be closed", thrown.getFirst()); + thrown.subList(1, thrown.size()).forEach(ex::addSuppressed); + throw ex; + } + } } } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ResourcePoolManager.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ResourcePoolManager.java index ba04b9db014..c49b8971e06 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ResourcePoolManager.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ResourcePoolManager.java @@ -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 moduleContent = new LinkedHashMap<>(); // lazily initialized @@ -132,16 +120,8 @@ public class ResourcePoolManager { public Set packages() { Set 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. + * + *

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 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 { diff --git a/test/jdk/jdk/internal/jimage/ImageLocationTest.java b/test/jdk/jdk/internal/jimage/ImageLocationTest.java new file mode 100644 index 00000000000..c6837c38265 --- /dev/null +++ b/test/jdk/jdk/internal/jimage/ImageLocationTest.java @@ -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 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 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 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); + } +} diff --git a/test/jdk/jdk/internal/jimage/ImageReaderTest.java b/test/jdk/jdk/internal/jimage/ImageReaderTest.java index de52ed1503d..b8f7763e97e 100644 --- a/test/jdk/jdk/internal/jimage/ImageReaderTest.java +++ b/test/jdk/jdk/internal/jimage/ImageReaderTest.java @@ -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//META-INF/preview/...' namespace. private static final Map> 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 pkgNames = root.getChildNames().collect(Collectors.toSet()); + Set 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 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 { diff --git a/test/jdk/jdk/internal/jimage/JImageReadTest.java b/test/jdk/jdk/internal/jimage/JImageReadTest.java index 35fb2adb687..192ca8a0ec9 100644 --- a/test/jdk/jdk/internal/jimage/JImageReadTest.java +++ b/test/jdk/jdk/internal/jimage/JImageReadTest.java @@ -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 diff --git a/test/jdk/jdk/internal/jimage/ModuleLinkTest.java b/test/jdk/jdk/internal/jimage/ModuleLinkTest.java new file mode 100644 index 00000000000..e87dd03f37d --- /dev/null +++ b/test/jdk/jdk/internal/jimage/ModuleLinkTest.java @@ -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 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 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 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 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 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 normalOffsets = asList(ModuleLink.readNameOffsets(buffer, true, false)); + List previewOffsets = asList(ModuleLink.readNameOffsets(buffer, false, true)); + List 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 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 List asList(Iterator src) { + List list = new ArrayList<>(); + src.forEachRemaining(list::add); + return list; + } + + // Encodes strings sequentially starting from index 100. + private static Function fakeEncoder() { + List cache = new ArrayList<>(); + return s -> { + int i = cache.indexOf(s); + if (i == -1) { + cache.add(s); + return 100 + (cache.size() - 1); + } else { + return 100 + i; + } + }; + } +} diff --git a/test/jdk/tools/jimage/ImageReaderDuplicateChildNodesTest.java b/test/jdk/tools/jimage/ImageReaderDuplicateChildNodesTest.java index f8f6894df10..d6369a478e0 100644 --- a/test/jdk/tools/jimage/ImageReaderDuplicateChildNodesTest.java +++ b/test/jdk/tools/jimage/ImageReaderDuplicateChildNodesTest.java @@ -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) { diff --git a/test/jdk/tools/jimage/VerifyJimage.java b/test/jdk/tools/jimage/VerifyJimage.java index 723ad089fa9..8d89233f0f3 100644 --- a/test/jdk/tools/jimage/VerifyJimage.java +++ b/test/jdk/tools/jimage/VerifyJimage.java @@ -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) { diff --git a/test/jdk/tools/jlink/JLinkPreviewTest.java b/test/jdk/tools/jlink/JLinkPreviewTest.java new file mode 100644 index 00000000000..d263c1ad972 --- /dev/null +++ b/test/jdk/tools/jlink/JLinkPreviewTest.java @@ -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 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 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"; + } +} diff --git a/test/micro/org/openjdk/bench/jdk/internal/jrtfs/ImageReaderBenchmark.java b/test/micro/org/openjdk/bench/jdk/internal/jrtfs/ImageReaderBenchmark.java index b6876e66d98..4da6d850ed0 100644 --- a/test/micro/org/openjdk/bench/jdk/internal/jrtfs/ImageReaderBenchmark.java +++ b/test/micro/org/openjdk/bench/jdk/internal/jrtfs/ImageReaderBenchmark.java @@ -24,6 +24,7 @@ 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 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);