/
githubmirror
/
riscv-port
Обзор
Документация
Войти
/
githubmirror
/
riscv-port
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/java.base/share/classes/jdk/internal/jimage/ImageReader.java
1 122 строки
48 KB
Du�an B�lek
8386842: Preview files in root directory not recognized in system image
02 июл 2026, 12:32
02 июл 2026, 12:32
c042ad2
Код
Авторство
О чём код?
/* * 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 * 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 jdk.internal.jimage.ImageLocation.LocationType; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.IntBuffer; import java.nio.file.Files; import java.nio.file.Path; 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 * information) are mapped into a unified hierarchy of named nodes, which serve * as the underlying structure for {@code JrtFileSystem} and other utilities. * * <p>Entries in jimage are expressed as one of three {@link Node} types; * resource nodes, directory nodes and link nodes. * * <p>When remapping jimage entries, jimage location names (e.g. {@code * "/java.base/java/lang/Integer.class"}) are prefixed with {@code "/modules"} * to form the names of resource nodes. This aligns with the naming of module * entries in jimage (e.g. "/modules/java.base/java/lang"), which appear as * directory nodes in {@code ImageReader}. * * <p>Package entries (e.g. {@code "/packages/java.lang"} appear as directory * nodes containing link nodes, which resolve back to the root directory of the * module in which that package exists (e.g. {@code "/modules/java.base"}). * Unlike other nodes, the jimage file does not contain explicit entries for * link nodes, and their existence is derived only from the contents of the * parent directory. * * <p>While similar to {@code BasicImageReader}, this class is not a conceptual * subtype of it, and deliberately hides types such as {@code ImageLocation} to * give a focused API based only on nodes. * * @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 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; private ImageReader(SharedImageReader reader) { this.reader = reader; } /** * 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, PreviewMode mode) throws IOException { return open(imagePath, ByteOrder.nativeOrder(), mode); } /** * 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, ByteOrder byteOrder, PreviewMode mode) throws IOException { Objects.requireNonNull(imagePath); Objects.requireNonNull(byteOrder); return SharedImageReader.open(imagePath, byteOrder, mode.isPreviewModeEnabled()); } @Override public void close() throws IOException { if (closed) { throw new IOException("image file already closed"); } reader.close(this); closed = true; } private void ensureOpen() throws IOException { if (closed) { throw new IOException("image file closed"); } } private void requireOpen() { if (closed) { throw new IllegalStateException("image file closed"); } } /** * Finds the node with the given name. * * @param name a node name of the form {@code "/modules/<module>/...} or * {@code "/packages/<package>/...}. * @return a node representing a resource, directory or symbolic link. */ public Node findNode(String name) throws IOException { ensureOpen(); return reader.findNode(name); } /** * Returns a resource node in the given module, or null if no resource of * that name exists. * * <p>This is equivalent to: * <pre>{@code * findNode("/modules/" + moduleName + "/" + resourcePath) * }</pre> * but more performant, and returns {@code null} for directories. * * @param moduleName The module name of the requested resource. * @param resourcePath Trailing module-relative resource path, not starting * with {@code '/'}. */ public Node findResourceNode(String moduleName, String resourcePath) throws IOException { ensureOpen(); return reader.findResourceNode(moduleName, resourcePath); } /** * Returns whether a resource exists in the given module. * * <p>This is equivalent to: * <pre>{@code * findResourceNode(moduleName, resourcePath) != null * }</pre> * but more performant, and will not create or cache new nodes. * * @param moduleName The module name of the resource being tested for. * @param resourcePath Trailing module-relative resource path, not starting * with {@code '/'}. */ public boolean containsResource(String moduleName, String resourcePath) throws IOException { ensureOpen(); return reader.containsResource(moduleName, resourcePath); } /** * Returns a copy of the content of a resource node. The buffer returned by * this method is not cached by the node, and each call returns a new array * instance. * * @throws IOException if the content cannot be returned (including if the * given node is not a resource node). */ public byte[] getResource(Node node) throws IOException { ensureOpen(); return reader.getResource(node); } /** * Returns the content of a resource node in a newly allocated byte buffer. */ public ByteBuffer getResourceBuffer(Node node) { requireOpen(); if (!node.isResource()) { throw new IllegalArgumentException("Not a resource node: " + node); } return reader.getResourceBuffer(node.getLocation()); } // Package protected for use only by SystemImageReader. ResourceEntries getResourceEntries() { return reader.getResourceEntries(); } private static final class SharedImageReader extends BasicImageReader { // There are >30,000 nodes in a complete jimage tree, and even relatively // common tasks (e.g. starting up javac) load somewhere in the region of // 1000 classes. Thus, an initial capacity of 2000 is a reasonable guess. private static final int INITIAL_NODE_CACHE_CAPACITY = 2000; static final class ReaderKey { private final Path imagePath; private final boolean isPreviewEnabled; public ReaderKey(Path imagePath, boolean isPreviewEnabled) { this.imagePath = imagePath; this.isPreviewEnabled = isPreviewEnabled; } @Override public boolean equals(Object obj) { // No pattern variables here (Java 8 compatible source). if (obj instanceof ReaderKey) { ReaderKey other = (ReaderKey) obj; return this.imagePath.equals(other.imagePath) && this.isPreviewEnabled == other.isPreviewEnabled; } return false; } @Override public int hashCode() { return imagePath.hashCode() ^ Boolean.hashCode(isPreviewEnabled); } } private static final Map<ReaderKey, SharedImageReader> OPEN_FILES = new HashMap<>(); // List of openers for this shared image. private final Set<ImageReader> openers = new HashSet<>(); // Attributes of the jimage file. The jimage file does not contain // attributes for the individual resources (yet). We use attributes // of the jimage file itself (creation, modification, access times). private final BasicFileAttributes imageFileAttributes; // Cache of all user visible nodes, guarded by synchronizing 'this' instance. private final Map<String, Node> nodes; // Preview mode support. private final boolean isPreviewEnabled; // A relativized mapping from non-preview name to directories containing // preview-only nodes. This is used to merge preview-only content into // directories as they are completed. // E.g. "/modules/xxx/foo/bar" -> Directory("/modules/xxx/META-INF/preview/foo/bar") private final Map<String, Directory> previewDirectoriesToMerge; private SharedImageReader(Path imagePath, ByteOrder byteOrder, boolean isPreviewEnabled) throws IOException { super(imagePath, byteOrder); this.imageFileAttributes = Files.readAttributes(imagePath, BasicFileAttributes.class); this.nodes = new HashMap<>(INITIAL_NODE_CACHE_CAPACITY); 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 = ensureCached(newDirectory(PACKAGES_PREFIX)); Directory modules = ensureCached(newDirectory(MODULES_PREFIX)); Directory root = newDirectory("/"); root.setChildren(Arrays.asList(packages, modules)); 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)); } /** * Process {@code "/packages/xxx"} entries to build the child nodes for the * root {@code "/packages"} node. Preview-only entries will be skipped if * {@code previewMode == false}. * * <p>If {@code previewMode == true}, this method also populates the {@link * #previewDirectoriesToMerge} map with any preview-only nodes, to be merged * into directories as they are completed. It also caches preview resources * and preview-only directories for direct lookup. */ private ArrayList<Node> processPackagesDirectory(boolean previewMode) { ImageLocation pkgRoot = findLocation(PACKAGES_PREFIX); assert pkgRoot != null : "Invalid jimage file"; IntBuffer offsets = getOffsetBuffer(pkgRoot); ArrayList<Node> pkgDirs = new ArrayList<>(offsets.capacity()); // Package path to module map, sorted in reverse order so that // longer child paths get processed first. Map<String, List<String>> previewPackagesToModules = new TreeMap<>(Comparator.reverseOrder()); for (int i = 0; i < offsets.capacity(); i++) { ImageLocation pkgDir = getLocation(offsets.get(i)); int flags = pkgDir.getFlags(); // A package subdirectory is "preview only" if all the modules // it references have that package marked as preview only. // Skipping these entries avoids empty package subdirectories. if (previewMode || !ImageLocation.isPreviewOnly(flags)) { pkgDirs.add(ensureCached(newDirectory(pkgDir.getFullName()))); } if (previewMode && ImageLocation.hasPreviewVersion(flags)) { // Only do this in preview mode for the small set of packages with // preview versions (the number of preview entries should be small). List<String> moduleNames = new ArrayList<>(); ModuleLink.readNameOffsets(getOffsetBuffer(pkgDir), /*normal*/ false, /*preview*/ true) .forEachRemaining(n -> moduleNames.add(getString(n))); previewPackagesToModules.put(pkgDir.getBase().replace('.', '/'), moduleNames); } } // Reverse sorted map means child directories are processed first. previewPackagesToModules.forEach((pkgPath, modules) -> modules.forEach(modName -> processPreviewDir(MODULES_PREFIX + "/" + modName, pkgPath))); // We might have skipped some preview-only package entries. pkgDirs.trimToSize(); return pkgDirs; } void processPreviewDir(String namePrefix, String pkgPath) { String previewDirName = namePrefix + PREVIEW_INFIX + "/" + pkgPath; ImageLocation previewLoc = findLocation(previewDirName); assert previewLoc != null : "Missing preview directory location: " + previewDirName; String nonPreviewDirName = namePrefix + "/" + pkgPath; List<Node> previewOnlyChildren = createChildNodes(previewLoc, 0, childLoc -> { String baseName = getBaseName(childLoc); String nonPreviewChildName = nonPreviewDirName + "/" + baseName; boolean isPreviewOnly = ImageLocation.isPreviewOnly(childLoc.getFlags()); LocationType type = childLoc.getType(); if (type == RESOURCE) { // Preview resources are cached to override non-preview versions. Node childNode = ensureCached(newResource(nonPreviewChildName, childLoc)); return isPreviewOnly ? childNode : null; } else { // Child directories are not cached here (they are either cached // already or have been added to previewDirectoriesToMerge). assert type == MODULES_DIR : "Invalid location type: " + childLoc; Node childNode = nodes.get(nonPreviewChildName); assert isPreviewOnly == (childNode != null) : "Inconsistent child node: " + nonPreviewChildName; return childNode; } }); Directory previewDir = newDirectory(nonPreviewDirName); previewDir.setChildren(previewOnlyChildren); if (ImageLocation.isPreviewOnly(previewLoc.getFlags())) { // If we are preview-only, our children are also preview-only, so // this directory is a complete hierarchy and should be cached. assert !previewOnlyChildren.isEmpty() : "Invalid empty preview-only directory: " + nonPreviewDirName; ensureCached(previewDir); } else if (!previewOnlyChildren.isEmpty()) { // A partial directory containing extra preview-only nodes // to be merged when the non-preview directory is completed. previewDirectoriesToMerge.put(nonPreviewDirName, previewDir); } } // Adds a node to the cache, ensuring that no matching entry already existed. private <T extends Node> T ensureCached(T node) { Node existingNode = nodes.put(node.getName(), node); assert existingNode == null : "Unexpected node already cached for: " + node; return node; } private static ImageReader open(Path imagePath, ByteOrder byteOrder, boolean previewMode) throws IOException { Objects.requireNonNull(imagePath); Objects.requireNonNull(byteOrder); synchronized (OPEN_FILES) { 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, previewMode); OPEN_FILES.put(key, reader); } else if (reader.getByteOrder() != byteOrder) { throw new IOException("\"" + reader.getName() + "\" is not an image file"); } ImageReader image = new ImageReader(reader); reader.openers.add(image); return image; } } public void close(ImageReader image) throws IOException { Objects.requireNonNull(image); synchronized (OPEN_FILES) { if (!openers.remove(image)) { throw new IOException("image file already closed"); } if (openers.isEmpty()) { close(); nodes.clear(); if (!OPEN_FILES.remove(new ReaderKey(getImagePath(), isPreviewEnabled), this)) { throw new IOException("image file not found in open list"); } } } } /** * Returns a node with the given name, or null if no resource or directory of * that name exists. * * <p>Note that there is no reentrant calling back to this method from within * the node handling code. * * @param name an absolute, {@code /}-separated path string, prefixed with either * "/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) { 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. assert node instanceof Directory : "Invalid incomplete node: " + node; completeDirectory((Directory) node); } assert node == null || node.isCompleted() : "Incomplete node: " + node; return node; } /** * Returns a resource node in the given module, or null if no resource of * that name exists. * * <p>Note that there is no reentrant calling back to this method from within * the node handling code. */ Node findResourceNode(String moduleName, String resourcePath) { // Unlike findNode(), this method can only reliably return resource nodes. if (moduleName.indexOf('/') >= 0) { throw new IllegalArgumentException("invalid module name: " + moduleName); } 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 = 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); } return node; } else { return node.isResource() ? node : null; } } } /** * Returns whether a resource exists in the given module. * * <p>This method is expected to be called frequently for resources * which do not exist in the given module (e.g. as part of classpath * search). As such, it skips checking the nodes cache 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 (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; } /** * Builds a node in the "/modules/..." namespace. * * <p>Called by {@link #findNode(String)} if a {@code /modules/...} node * is not present in the cache. */ 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 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. String resourceName = name.substring(MODULES_PREFIX.length()); if (isPreviewEnabled) { // Root-level preview resources are not pre-cached when an image // is opened, so check for them first. int pathStart = resourceName.indexOf('/', 1); if (pathStart > 1 && resourceName.indexOf('/', pathStart + 1) < 0) { loc = findLocation(resourceName.substring(0, pathStart) + PREVIEW_INFIX + "/" + resourceName.substring(pathStart + 1)); } } if (loc == null) { loc = findLocation(resourceName); } return loc != null && loc.getType() == RESOURCE ? ensureCached(newResource(name, loc)) : null; } /** * Returns whether a directory name in the "/modules/" directory could be referencing * the "META-INF" directory". */ private boolean isMetaInf(Directory dir) { String name = dir.getName(); int pathStart = name.indexOf('/', MODULES_PREFIX.length() + 1); return name.length() == pathStart + "/META-INF".length() && name.endsWith("/META-INF"); } /** * Returns whether a node name in the "/modules/" directory could be referencing * a preview resource or directory under "META-INF/preview". */ private boolean isPreviewName(String name) { int pathStart = name.indexOf('/', MODULES_PREFIX.length() + 1); int previewEnd = pathStart + PREVIEW_INFIX.length(); return pathStart > 0 && name.regionMatches(pathStart, PREVIEW_INFIX, 0, PREVIEW_INFIX.length()) && (name.length() == previewEnd || name.charAt(previewEnd) == '/'); } private String getBaseName(ImageLocation loc) { // Matches logic in ImageLocation#getFullName() regarding extensions. String trailingParts = loc.getBase() + ((loc.getExtensionOffset() != 0) ? "." + loc.getExtension() : ""); return trailingParts.substring(trailingParts.lastIndexOf('/') + 1); } /** * Builds a link node of the form "/packages/xxx/yyy". * * <p>Called by {@link #findNode(String)} if a {@code /packages/...} * node is not present in the cache (the name is not trusted). */ private Node buildAndCacheLinkNode(String name) { // There are only locations for "/packages" or "/packages/xxx" // directories, but not the symbolic links below them (links are // derived from the name information in the parent directory). int packageStart = PACKAGES_PREFIX.length() + 1; int packageEnd = name.indexOf('/', packageStart); // 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 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; } /** Completes a directory by ensuring its child list is populated correctly. */ private void completeDirectory(Directory dir) { String name = dir.getName(); // 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_PREFIX) || name.startsWith(PACKAGES_PREFIX); ImageLocation loc = findLocation(name); assert loc != null && name.equals(loc.getFullName()) : "Invalid location for name: " + name; LocationType type = loc.getType(); if (type == MODULES_DIR || type == MODULES_ROOT) { completeModuleDirectory(dir, loc); } else { 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. */ private Directory completeModuleDirectory(Directory dir, ImageLocation loc) { assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir; List<Node> previewOnlyNodes = getPreviewNodesToMerge(dir); if (isPreviewEnabled && previewOnlyNodes.isEmpty()) { // When opening an image in preview mode, packages that have preview // content are eagerly processed, caching preview resources and // preview-only directories for direct lookup. Root-level preview // resources are omitted during this process, since they have no // package path and the empty package is not represented under // "/packages", and must be processed separately. int moduleStart = MODULES_PREFIX.length() + 1; if (dir.getName().indexOf('/', moduleStart) < 0) { ImageLocation previewLoc = findLocation(dir.getName() + PREVIEW_INFIX); if (previewLoc != null) { previewOnlyNodes = createChildNodes(previewLoc, 0, childLoc -> { String baseName = getBaseName(childLoc); String nonPreviewChildName = dir.getName() + "/" + baseName; boolean isPreviewOnly = ImageLocation.isPreviewOnly(childLoc.getFlags()); LocationType type = childLoc.getType(); if (type == RESOURCE) { Node childNode = nodes.computeIfAbsent(nonPreviewChildName, n -> newResource(n, childLoc)); return isPreviewOnly ? childNode : null; } else { assert type == MODULES_DIR : "Invalid location type: " + childLoc; Node childNode = nodes.get(nonPreviewChildName); assert !(isPreviewOnly && childNode == null) : "Inconsistent child node: " + nonPreviewChildName; return isPreviewOnly ? childNode : null; } }); } } } // We hide preview names from direct lookup, but must also prevent // the preview directory from appearing in any META-INF directories. boolean parentIsMetaInfDir = isMetaInf(dir); List<Node> children = createChildNodes(loc, previewOnlyNodes.size(), childLoc -> { LocationType type = childLoc.getType(); if (type == MODULES_DIR) { String name = childLoc.getFullName(); return parentIsMetaInfDir && name.endsWith("/preview") ? null : nodes.computeIfAbsent(name, this::newDirectory); } else { assert type == RESOURCE : "Invalid location type: " + loc; // Add "/modules" prefix to image location paths to get node names. String resourceName = childLoc.getFullName(true); return nodes.computeIfAbsent(resourceName, n -> newResource(n, childLoc)); } }); children.addAll(previewOnlyNodes); dir.setChildren(children); return dir; } /** Completes a package directory by setting the list of child nodes. */ private void completePackageSubdirectory(Directory dir, ImageLocation loc) { assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir; assert !dir.isCompleted() : "Directory already completed: " + dir; assert loc.getType() == PACKAGES_DIR : "Invalid location type: " + loc.getType(); // In non-preview mode we might skip a very small number of preview-only // entries, but it's not worth "right-sizing" the array for that. IntBuffer offsets = getOffsetBuffer(loc); List<Node> children = new ArrayList<>(offsets.capacity() / 2); ModuleLink.readNameOffsets(offsets, /*normal*/ true, isPreviewEnabled) .forEachRemaining(n -> { String modName = getString(n); Node link = newLinkNode(dir.getName() + "/" + modName, MODULES_PREFIX + "/" + modName); children.add(ensureCached(link)); }); // If the parent directory exists, there must be at least one child node. assert !children.isEmpty() : "Invalid empty package directory: " + dir; dir.setChildren(children); } /** * Returns the list of child preview nodes to be merged into the given directory. * * <p>Because this is only called once per-directory (since the result is cached * indefinitely) we can remove any entries we find from the cache. If ever the * node cache allowed entries to expire, this would have to be changed so that * directories could be completed more than once. */ List<Node> getPreviewNodesToMerge(Directory dir) { if (previewDirectoriesToMerge != null) { Directory mergeDir = previewDirectoriesToMerge.remove(dir.getName()); if (mergeDir != null) { return mergeDir.children; } } return Collections.emptyList(); } /** * Creates the list of child nodes for a modules {@code Directory} from * its parent location. * * <p>The {@code getChildFn} may return existing cached nodes rather * than creating them, and if newly created nodes are to be cached, * it is the job of {@code getChildFn}, or the caller of this method, * to do that. * * @param loc a location relating to a "/modules" directory. * @param extraNodesCount a known number of preview-only child nodes * which will be merged onto the end of the returned list later. * @param getChildFn a function to return a node for each child location * (or null to skip putting anything in the list). * @return the list of the non-null child nodes, returned by * {@code getChildFn}, in the order of the locations entries. */ private List<Node> createChildNodes(ImageLocation loc, int extraNodesCount, Function<ImageLocation, Node> getChildFn) { LocationType type = loc.getType(); assert type == MODULES_DIR || type == MODULES_ROOT : "Invalid location type: " + loc; IntBuffer offsets = getOffsetBuffer(loc); int childCount = offsets.capacity(); List<Node> children = new ArrayList<>(childCount + extraNodesCount); for (int i = 0; i < childCount; 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 dir.getType() != RESOURCE : "Not a directory: " + dir.getFullName(); byte[] offsets = getResource(dir); ByteBuffer buffer = ByteBuffer.wrap(offsets); buffer.order(getByteOrder()); return buffer.asIntBuffer(); } /** * Creates an "incomplete" directory node with no child nodes set. * Directories need to be "completed" before they are returned by * {@link #findNode(String)}. */ private Directory newDirectory(String name) { return new Directory(name, imageFileAttributes); } /** * Creates a new resource from an image location. This is the only case * where the image location name does not match the requested node name. * In image files, resource locations are NOT prefixed by {@code /modules}. */ private Resource newResource(String name, ImageLocation loc) { return new Resource(name, loc, imageFileAttributes); } /** * Creates a new link node pointing at the given target name. * * <p>Note that target node is resolved each time {@code resolve()} is called, * so if a link node is retained after its reader is closed, it will fail. */ private LinkNode newLinkNode(String name, String targetName) { return new LinkNode(name, () -> findNode(targetName), imageFileAttributes); } /** Returns the content of a resource node. */ private byte[] getResource(Node node) throws IOException { // We could have been given a non-resource node here. if (node.isResource()) { return super.getResource(node.getLocation()); } throw new IOException("Not a resource: " + node); } } /** * A directory, resource or symbolic link. * * <h3 id="node_equality">Node Equality</h3> * * Nodes are identified solely by their name, and it is not valid to attempt * to compare nodes from different reader instances. Different readers may * produce nodes with the same names, but different contents. * * <p>Furthermore, since a {@link ImageReader} provides "perfect" caching of * nodes, equality of nodes from the same reader is equivalent to instance * identity. */ public abstract static class Node { private final String name; private final BasicFileAttributes fileAttrs; /** * Creates an abstract {@code Node}, which is either a resource, directory * or symbolic link. * * <p>This constructor is only non-private so it can be used by the * {@code ExplodedImage} class, and must not be used otherwise. */ protected Node(String name, BasicFileAttributes fileAttrs) { this.name = Objects.requireNonNull(name); this.fileAttrs = Objects.requireNonNull(fileAttrs); } // A node is completed when all its direct children have been built. // As such, non-directory nodes are always complete. boolean isCompleted() { return true; } // Only resources can return a location. ImageLocation getLocation() { throw new IllegalStateException("not a resource: " + getName()); } /** * Returns the name of this node (e.g. {@code * "/modules/java.base/java/lang/Object.class"} or {@code * "/packages/java.lang"}). * * <p>Note that for resource nodes this is NOT the underlying jimage * resource name (it is prefixed with {@code "/modules"}). */ public final String getName() { return name; } /** * Returns file attributes for this node. The value returned may be the * same for all nodes, and should not be relied upon for accuracy. */ public final BasicFileAttributes getFileAttributes() { return fileAttrs; } /** * Resolves a symbolic link to its target node. If this code is not a * symbolic link, then it resolves to itself. */ public final Node resolveLink() { return resolveLink(false); } /** * Resolves a symbolic link to its target node. If this code is not a * symbolic link, then it resolves to itself. */ public Node resolveLink(boolean recursive) { return this; } /** Returns whether this node is a symbolic link. */ public boolean isLink() { return false; } /** * Returns whether this node is a directory. Directory nodes can have * {@link #getChildNames()} invoked to get the fully qualified names * of any child nodes. */ public boolean isDirectory() { return false; } /** * Returns whether this node is a resource. Resource nodes can have * their contents obtained via {@link ImageReader#getResource(Node)} * or {@link ImageReader#getResourceBuffer(Node)}. */ public boolean isResource() { return false; } /** * Returns the fully qualified names of any child nodes for a directory. * * <p>By default, this method throws {@link IllegalStateException} and * is overridden for directories. */ public Stream<String> getChildNames() { throw new IllegalStateException("not a directory: " + getName()); } /** * Returns the uncompressed size of this node's content. If this node is * not a resource, this method returns zero. */ public long size() { return 0L; } /** * Returns the compressed size of this node's content. If this node is * not a resource, this method returns zero. */ public long compressedSize() { return 0L; } /** * Returns the extension string of a resource node. If this node is not * a resource, this method returns null. */ public String extension() { return null; } @Override public final String toString() { return getName(); } /** See <a href="#node_equality">Node Equality</a>. */ @Override public final int hashCode() { return name.hashCode(); } /** See <a href="#node_equality">Node Equality</a>. */ @Override public final boolean equals(Object other) { if (this == other) { return true; } if (other instanceof Node) { return name.equals(((Node) other).name); } return false; } } /** * Directory node (referenced from a full path, without a trailing '/'). * * <p>Directory nodes have two distinct states: * <ul> * <li>Incomplete: The child list has not been set. * <li>Complete: The child list has been set. * </ul> * * <p>When a directory node is returned by {@link ImageReader#findNode(String)} * it is always complete, but this DOES NOT mean that its child nodes are * complete yet. * * <p>To avoid users being able to access incomplete child nodes, the * {@code Node} API offers only a way to obtain child node names, forcing * callers to invoke {@code findNode()} if they need to access the child * node itself. * * <p>This approach allows directories to be implemented lazily with respect * to child nodes, while retaining efficiency when child nodes are accessed * (since any incomplete nodes will be created and placed in the node cache * when the parent was first returned to the user). */ private static final class Directory extends Node { // Monotonic reference, will be set to the unmodifiable child list exactly once. private List<Node> children = null; private Directory(String name, BasicFileAttributes fileAttrs) { super(name, fileAttrs); } @Override boolean isCompleted() { return children != null; } @Override public boolean isDirectory() { return true; } @Override public Stream<String> getChildNames() { if (children != null) { return children.stream().map(Node::getName); } throw new IllegalStateException("Cannot get child nodes of an incomplete directory: " + getName()); } private void setChildren(List<? extends Node> children) { assert this.children == null : this + ": Cannot set child nodes twice!"; this.children = Collections.unmodifiableList(children); } } /** * Resource node (e.g. a ".class" entry, or any other data resource). * * <p>Resources are leaf nodes referencing an underlying image location. They * are lightweight, and do not cache their contents. * * <p>Unlike directories (where the node name matches the jimage path for the * corresponding {@code ImageLocation}), resource node names are NOT the same * as the corresponding jimage path. The difference is that node names for * resources are prefixed with "/modules", which is missing from the * equivalent jimage path. */ private static class Resource extends Node { private final ImageLocation loc; private Resource(String name, ImageLocation loc, BasicFileAttributes fileAttrs) { super(name, fileAttrs); this.loc = loc; } @Override ImageLocation getLocation() { return loc; } @Override public boolean isResource() { return true; } @Override public long size() { return loc.getUncompressedSize(); } @Override public long compressedSize() { return loc.getCompressedSize(); } @Override public String extension() { return loc.getExtension(); } } /** * Link node (a symbolic link to a top-level modules directory). * * <p>Link nodes resolve their target by invoking a given supplier, and do * not cache the result. Since nodes are cached by the {@code ImageReader}, * this means that only the first call to {@link #resolveLink(boolean)} * could do any significant work. */ private static class LinkNode extends Node { private final Supplier<Node> link; private LinkNode(String name, Supplier<Node> link, BasicFileAttributes fileAttrs) { super(name, fileAttrs); this.link = link; } @Override public Node resolveLink(boolean recursive) { // No need to use or propagate the recursive flag, since the target // cannot possibly be a link node (links only point to directories). return link.get(); } @Override public boolean isLink() { return true; } } }