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 f12c39f3e81..79e718c76e5 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/ImageReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,16 +25,14 @@ package jdk.internal.jimage; import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.IntBuffer; import java.nio.file.Files; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.FileTime; 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.HashMap; import java.util.HashSet; @@ -42,9 +40,36 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; /** + * 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. + * + *

Entries in jimage are expressed as one of three {@link Node} types; + * resource nodes, directory nodes and link nodes. + * + *

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

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

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, @@ -60,6 +85,10 @@ public final class ImageReader implements AutoCloseable { this.reader = reader; } + /** + * Opens an image reader for a jimage file at the specified path, using the + * given byte order. + */ public static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException { Objects.requireNonNull(imagePath); Objects.requireNonNull(byteOrder); @@ -67,6 +96,10 @@ public final class ImageReader implements AutoCloseable { return SharedImageReader.open(imagePath, byteOrder); } + /** + * Opens an image reader for a jimage file at the specified path, using the + * platform native byte order. + */ public static ImageReader open(Path imagePath) throws IOException { return open(imagePath, ByteOrder.nativeOrder()); } @@ -92,146 +125,113 @@ public final class ImageReader implements AutoCloseable { } } - // directory management interface - public Directory getRootDirectory() throws IOException { - ensureOpen(); - return reader.getRootDirectory(); - } - - + /** + * Finds the node with the given name. + * + * @param name a node name of the form {@code "/modules//...} or + * {@code "/packages//...}. + * @return a node representing a resource, directory or symbolic link. + */ public Node findNode(String name) throws IOException { ensureOpen(); return reader.findNode(name); } + /** + * 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); } - public byte[] getResource(Resource rs) throws IOException { - ensureOpen(); - return reader.getResource(rs); - } - - public ImageHeader getHeader() { - requireOpen(); - return reader.getHeader(); - } - + /** + * Releases a (possibly cached) {@link ByteBuffer} obtained via + * {@link #getResourceBuffer(Node)}. + * + *

Note that no testing is performed to check whether the buffer about + * to be released actually came from a call to {@code getResourceBuffer()}. + */ public static void releaseByteBuffer(ByteBuffer buffer) { BasicImageReader.releaseByteBuffer(buffer); } - public String getName() { + /** + * Returns the content of a resource node in a possibly cached byte buffer. + * Callers of this method must call {@link #releaseByteBuffer(ByteBuffer)} + * when they are finished with it. + */ + public ByteBuffer getResourceBuffer(Node node) { requireOpen(); - return reader.getName(); - } - - public ByteOrder getByteOrder() { - requireOpen(); - return reader.getByteOrder(); - } - - public Path getImagePath() { - requireOpen(); - return reader.getImagePath(); - } - - public ImageStringsReader getStrings() { - requireOpen(); - return reader.getStrings(); - } - - public ImageLocation findLocation(String mn, String rn) { - requireOpen(); - return reader.findLocation(mn, rn); - } - - public boolean verifyLocation(String mn, String rn) { - requireOpen(); - return reader.verifyLocation(mn, rn); - } - - public ImageLocation findLocation(String name) { - requireOpen(); - return reader.findLocation(name); - } - - public String[] getEntryNames() { - requireOpen(); - return reader.getEntryNames(); - } - - public String[] getModuleNames() { - requireOpen(); - int off = "/modules/".length(); - return reader.findNode("/modules") - .getChildren() - .stream() - .map(Node::getNameString) - .map(s -> s.substring(off, s.length())) - .toArray(String[]::new); - } - - public long[] getAttributes(int offset) { - requireOpen(); - return reader.getAttributes(offset); - } - - public String getString(int offset) { - requireOpen(); - return reader.getString(offset); - } - - public byte[] getResource(String name) { - requireOpen(); - return reader.getResource(name); - } - - public byte[] getResource(ImageLocation loc) { - requireOpen(); - return reader.getResource(loc); - } - - public ByteBuffer getResourceBuffer(ImageLocation loc) { - requireOpen(); - return reader.getResourceBuffer(loc); - } - - public InputStream getResourceStream(ImageLocation loc) { - requireOpen(); - return reader.getResourceStream(loc); + if (!node.isResource()) { + throw new IllegalArgumentException("Not a resource node: " + node); + } + return reader.getResourceBuffer(node.getLocation()); } private static final class SharedImageReader extends BasicImageReader { - static final int SIZE_OF_OFFSET = Integer.BYTES; - - static final Map OPEN_FILES = new HashMap<>(); + 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; // List of openers for this shared image. - final Set openers; + private final Set openers = new HashSet<>(); - // attributes of the .jimage file. jimage file does not contain + // 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). - // Iniitalized lazily, see {@link #imageFileAttributes()}. - BasicFileAttributes imageFileAttributes; + private final BasicFileAttributes imageFileAttributes; - // directory management implementation - final HashMap nodes; - volatile Directory rootDir; - - Directory packagesDir; - Directory modulesDir; + // 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 { super(imagePath, byteOrder); - this.openers = new HashSet<>(); - this.nodes = new HashMap<>(); + 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"); + + // 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 root = newDirectory("/"); + root.setChildren(Arrays.asList(packages, modules)); + nodes.put(root.getName(), root); } - public static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException { + /** + * 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. + */ + 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 static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException { Objects.requireNonNull(imagePath); Objects.requireNonNull(byteOrder); @@ -264,7 +264,6 @@ public final class ImageReader implements AutoCloseable { if (openers.isEmpty()) { close(); nodes.clear(); - rootDir = null; if (!OPEN_FILES.remove(this.getImagePath(), this)) { throw new IOException("image file not found in open list"); @@ -273,448 +272,412 @@ public final class ImageReader implements AutoCloseable { } } - void addOpener(ImageReader reader) { - synchronized (OPEN_FILES) { - openers.add(reader); + /** + * Returns a node with the given name, or null if no resource or directory of + * that name exists. + * + *

This is the only public API by which anything outside this class can access + * {@code Node} instances either directly, or by resolving symbolic links. + * + *

Note also 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) { + 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 + "/")) { + node = buildModulesNode(name); + } else if (name.startsWith(PACKAGES_ROOT + "/")) { + node = buildPackagesNode(name); + } + if (node != null) { + nodes.put(node.getName(), node); + } + } else if (!node.isCompleted()) { + // Only directories can be incomplete. + assert node instanceof Directory : "Invalid incomplete node: " + node; + completeDirectory((Directory) node); } - } - - boolean removeOpener(ImageReader reader) { - synchronized (OPEN_FILES) { - return openers.remove(reader); - } - } - - // directory management interface - Directory getRootDirectory() { - return buildRootDirectory(); + assert node == null || node.isCompleted() : "Incomplete node: " + node; + return node; } /** - * Lazily build a node from a name. + * Builds a node in the "/modules/..." namespace. + * + *

Called by {@link #findNode(String)} if a {@code /modules/...} node + * is not present in the cache. */ - synchronized Node buildNode(String name) { - Node n; - boolean isPackages = name.startsWith("/packages"); - boolean isModules = !isPackages && name.startsWith("/modules"); - - if (!(isModules || isPackages)) { - return null; - } - + private Node buildModulesNode(String name) { + assert name.startsWith(MODULES_ROOT + "/") : "Invalid module node name: " + name; + // 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) { // A sub tree node - if (isPackages) { - n = handlePackages(name, loc); - } else { // modules sub tree - n = handleModulesSubTree(name, loc); - } - } else { // Asking for a resource? /modules/java.base/java/lang/Object.class - if (isModules) { - n = handleResource(name); - } else { - // Possibly ask for /packages/java.lang/java.base - // although /packages/java.base not created - n = handleModuleLink(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); } - return n; - } - - synchronized Directory buildRootDirectory() { - Directory root = rootDir; // volatile read - if (root != null) { - return root; - } - - root = newDirectory(null, "/"); - root.setIsRootDir(); - - // /packages dir - packagesDir = newDirectory(root, "/packages"); - packagesDir.setIsPackagesDir(); - - // /modules dir - modulesDir = newDirectory(root, "/modules"); - modulesDir.setIsModulesDir(); - - root.setCompleted(true); - return rootDir = root; + // 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; } /** - * To visit sub tree resources. + * Builds a node in the "/packages/..." namespace. + * + *

Called by {@link #findNode(String)} if a {@code /packages/...} node + * is not present in the cache. */ - interface LocationVisitor { - void visit(ImageLocation loc); - } - - void visitLocation(ImageLocation loc, LocationVisitor visitor) { - byte[] offsets = getResource(loc); - ByteBuffer buffer = ByteBuffer.wrap(offsets); - buffer.order(getByteOrder()); - IntBuffer intBuffer = buffer.asIntBuffer(); - for (int i = 0; i < offsets.length / SIZE_OF_OFFSET; i++) { - int offset = intBuffer.get(i); - ImageLocation pkgLoc = getLocation(offset); - visitor.visit(pkgLoc); - } - } - - void visitPackageLocation(ImageLocation loc) { - // Retrieve package name - String pkgName = getBaseExt(loc); - // Content is array of offsets in Strings table - byte[] stringsOffsets = getResource(loc); - ByteBuffer buffer = ByteBuffer.wrap(stringsOffsets); - buffer.order(getByteOrder()); - IntBuffer intBuffer = buffer.asIntBuffer(); - // For each module, create a link node. - for (int i = 0; i < stringsOffsets.length / SIZE_OF_OFFSET; i++) { - // skip empty state, useless. - intBuffer.get(i); - i++; - int offset = intBuffer.get(i); - String moduleName = getString(offset); - Node targetNode = findNode("/modules/" + moduleName); - if (targetNode != null) { - String pkgDirName = packagesDir.getName() + "/" + pkgName; - Directory pkgDir = (Directory) nodes.get(pkgDirName); - newLinkNode(pkgDir, pkgDir.getName() + "/" + moduleName, targetNode); - } - } - } - - Node handlePackages(String name, ImageLocation loc) { - long size = loc.getUncompressedSize(); - Node n = null; - // Only possibilities are /packages, /packages/package/module - if (name.equals("/packages")) { - visitLocation(loc, (childloc) -> { - findNode(childloc.getFullName()); - }); - packagesDir.setCompleted(true); - n = packagesDir; + 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; + int packageEnd = name.indexOf('/', packageStart); + if (packageEnd == -1) { + ImageLocation loc = findLocation(name); + return loc != null ? completePackageDirectory(newDirectory(name), loc) : null; } else { - if (size != 0) { // children are offsets to module in StringsTable - String pkgName = getBaseExt(loc); - Directory pkgDir = newDirectory(packagesDir, packagesDir.getName() + "/" + pkgName); - visitPackageLocation(loc); - pkgDir.setCompleted(true); - n = pkgDir; - } else { // Link to module - String pkgName = loc.getParent(); - String modName = getBaseExt(loc); - Node targetNode = findNode("/modules/" + modName); - if (targetNode != null) { - String pkgDirName = packagesDir.getName() + "/" + pkgName; - Directory pkgDir = (Directory) nodes.get(pkgDirName); - Node linkNode = newLinkNode(pkgDir, pkgDir.getName() + "/" + modName, targetNode); - n = linkNode; + // 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). + 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); } } } - return n; + return null; } - // Asking for /packages/package/module although - // /packages// not yet created, need to create it - // prior to return the link to module node. - Node handleModuleLink(String name) { - // eg: unresolved /packages/package/module - // Build /packages/package node - Node ret = null; - String radical = "/packages/"; - String path = name; - if (path.startsWith(radical)) { - int start = radical.length(); - int pkgEnd = path.indexOf('/', start); - if (pkgEnd != -1) { - String pkg = path.substring(start, pkgEnd); - String pkgPath = radical + pkg; - Node n = findNode(pkgPath); - // If not found means that this is a symbolic link such as: - // /packages/java.util/java.base/java/util/Vector.class - // and will be done by a retry of the filesystem - for (Node child : n.getChildren()) { - if (child.name.equals(name)) { - ret = child; - break; - } - } - } + /** 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_ROOT) || name.startsWith(PACKAGES_ROOT); + 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') { + completeModuleDirectory(dir, loc); + } else { + completePackageDirectory(dir, loc); } - return ret; + assert dir.isCompleted() : "Directory must be complete by now: " + dir; } - Node handleModulesSubTree(String name, ImageLocation loc) { - Node n; - assert (name.equals(loc.getFullName())); - Directory dir = makeDirectories(name); - visitLocation(loc, (childloc) -> { - String path = childloc.getFullName(); - if (path.startsWith("/modules")) { // a package - makeDirectories(path); - } else { // a resource - makeDirectories(childloc.buildName(true, true, false)); - // if we have already created a resource for this name previously, then don't - // recreate it - if (!nodes.containsKey(childloc.getFullName(true))) { - newResource(dir, childloc); - } + /** + * 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. + */ + 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); + } else { + // Add "/modules" prefix to image location paths to get node names. + String resourceName = childLoc.getFullName(true); + return nodes.computeIfAbsent(resourceName, n -> newResource(n, childLoc)); } }); - dir.setCompleted(true); - n = dir; - return n; - } - - Node handleResource(String name) { - Node n = null; - if (!name.startsWith("/modules/")) { - return null; - } - // Make sure that the thing that follows "/modules/" is a module name. - int moduleEndIndex = name.indexOf('/', "/modules/".length()); - if (moduleEndIndex == -1) { - return null; - } - ImageLocation moduleLoc = findLocation(name.substring(0, moduleEndIndex)); - if (moduleLoc == null || moduleLoc.getModuleOffset() == 0) { - return null; - } - - String locationPath = name.substring("/modules".length()); - ImageLocation resourceLoc = findLocation(locationPath); - if (resourceLoc != null) { - Directory dir = makeDirectories(resourceLoc.buildName(true, true, false)); - Resource res = newResource(dir, resourceLoc); - n = res; - } - return n; - } - - String getBaseExt(ImageLocation loc) { - String base = loc.getBase(); - String ext = loc.getExtension(); - if (ext != null && !ext.isEmpty()) { - base = base + "." + ext; - } - return base; - } - - synchronized Node findNode(String name) { - buildRootDirectory(); - Node n = nodes.get(name); - if (n == null || !n.isCompleted()) { - n = buildNode(name); - } - return n; - } - - /** - * Returns the file attributes of the image file. - */ - BasicFileAttributes imageFileAttributes() { - BasicFileAttributes attrs = imageFileAttributes; - if (attrs == null) { - try { - Path file = getImagePath(); - attrs = Files.readAttributes(file, BasicFileAttributes.class); - } catch (IOException ioe) { - throw new UncheckedIOException(ioe); - } - imageFileAttributes = attrs; - } - return attrs; - } - - Directory newDirectory(Directory parent, String name) { - Directory dir = Directory.create(parent, name, imageFileAttributes()); - nodes.put(dir.getName(), dir); + dir.setChildren(children); return dir; } - Resource newResource(Directory parent, ImageLocation loc) { - Resource res = Resource.create(parent, loc, imageFileAttributes()); - nodes.put(res.getName(), res); - return res; - } - - LinkNode newLinkNode(Directory dir, String name, Node link) { - LinkNode linkNode = LinkNode.create(dir, name, link); - nodes.put(linkNode.getName(), linkNode); - return linkNode; - } - - Directory makeDirectories(String parent) { - Directory last = rootDir; - for (int offset = parent.indexOf('/', 1); - offset != -1; - offset = parent.indexOf('/', offset + 1)) { - String dir = parent.substring(0, offset); - last = makeDirectory(dir, last); + /** + * 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) { + 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))); + } } - return makeDirectory(parent, last); - + // This only happens once and "completes" the directory. + dir.setChildren(children); + return dir; } - Directory makeDirectory(String dir, Directory last) { - Directory nextDir = (Directory) nodes.get(dir); - if (nextDir == null) { - nextDir = newDirectory(last, dir); + /** + * Creates the list of child nodes for a {@code Directory} based on a given + * + *

Note: This cannot be used for package subdirectories as they have + * child offsets stored differently to other directories. + */ + private List createChildNodes(ImageLocation loc, Function newChildFn) { + IntBuffer offsets = getOffsetBuffer(loc); + int childCount = offsets.capacity(); + List children = new ArrayList<>(childCount); + for (int i = 0; i < childCount; i++) { + children.add(newChildFn.apply(getLocation(offsets.get(i)))); } - return nextDir; + return children; } - byte[] getResource(Node node) throws IOException { + /** Helper to extract the integer offset buffer from a directory location. */ + private IntBuffer getOffsetBuffer(ImageLocation dir) { + assert !isResource(dir) : "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 + * {@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) { + assert name.equals(loc.getFullName(true)) : "Mismatched location for resource: " + name; + return new Resource(name, loc, imageFileAttributes); + } + + /** + * Creates a new link node pointing at the given target name. + * + *

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); } - - byte[] getResource(Resource rs) throws IOException { - return super.getResource(rs.getLocation()); - } } - // jimage file does not store directory structure. We build nodes - // using the "path" strings found in the jimage file. - // Node can be a directory or a resource + /** + * A directory, resource or symbolic link. + * + *

Node Equality

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

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 static final int ROOT_DIR = 0b0000_0000_0000_0001; - private static final int PACKAGES_DIR = 0b0000_0000_0000_0010; - private static final int MODULES_DIR = 0b0000_0000_0000_0100; - - private int flags; private final String name; private final BasicFileAttributes fileAttrs; - private boolean completed; + /** + * Creates an abstract {@code Node}, which is either a resource, directory + * or symbolic link. + * + *

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()); + } + /** - * A node is completed when all its direct children have been built. + * Returns the name of this node (e.g. {@code + * "/modules/java.base/java/lang/Object.class"} or {@code + * "/packages/java.lang"}). * - * @return + *

Note that for resource nodes this is NOT the underlying jimage + * resource name (it is prefixed with {@code "/modules"}). */ - public boolean isCompleted() { - return completed; - } - - public void setCompleted(boolean completed) { - this.completed = completed; - } - - public final void setIsRootDir() { - flags |= ROOT_DIR; - } - - public final boolean isRootDir() { - return (flags & ROOT_DIR) != 0; - } - - public final void setIsPackagesDir() { - flags |= PACKAGES_DIR; - } - - public final boolean isPackagesDir() { - return (flags & PACKAGES_DIR) != 0; - } - - public final void setIsModulesDir() { - flags |= MODULES_DIR; - } - - public final boolean isModulesDir() { - return (flags & MODULES_DIR) != 0; - } - 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; } - // resolve this Node (if this is a soft link, get underlying Node) + /** + * 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; } - // is this a soft link Node? + /** 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; } - public List getChildren() { - throw new IllegalArgumentException("not a directory: " + getNameString()); - } - + /** + * 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; } - public ImageLocation getLocation() { - throw new IllegalArgumentException("not a resource: " + getNameString()); + /** + * Returns the fully qualified names of any child nodes for a directory. + * + *

By default, this method throws {@link IllegalStateException} and + * is overridden for directories. + */ + public Stream 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; } - public long contentOffset() { - return 0L; - } - - public final FileTime creationTime() { - return fileAttrs.creationTime(); - } - - public final FileTime lastAccessTime() { - return fileAttrs.lastAccessTime(); - } - - public final FileTime lastModifiedTime() { - return fileAttrs.lastModifiedTime(); - } - - public final String getNameString() { - return name; - } - @Override public final String toString() { - return getNameString(); + return getName(); } + /** See Node Equality. */ @Override public final int hashCode() { return name.hashCode(); } + /** See Node Equality. */ @Override public final boolean equals(Object other) { if (this == other) { @@ -729,21 +692,40 @@ public final class ImageReader implements AutoCloseable { } } - // directory node - directory has full path name without '/' at end. - static final class Directory extends Node { - private final List children; + /** + * Directory node (referenced from a full path, without a trailing '/'). + * + *

Directory nodes have two distinct states: + *

+ * + *

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

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

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 children = null; private Directory(String name, BasicFileAttributes fileAttrs) { super(name, fileAttrs); - children = new ArrayList<>(); } - static Directory create(Directory parent, String name, BasicFileAttributes fileAttrs) { - Directory d = new Directory(name, fileAttrs); - if (parent != null) { - parent.addChild(d); - } - return d; + @Override + boolean isCompleted() { + return children != null; } @Override @@ -752,46 +734,41 @@ public final class ImageReader implements AutoCloseable { } @Override - public List getChildren() { - return Collections.unmodifiableList(children); - } - - void addChild(Node node) { - assert !children.contains(node) : "Child " + node + " already added"; - children.add(node); - } - - public void walk(Consumer consumer) { - consumer.accept(this); - for (Node child : children) { - if (child.isDirectory()) { - ((Directory)child).walk(consumer); - } else { - consumer.accept(child); - } + public Stream 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 children) { + assert this.children == null : this + ": Cannot set child nodes twice!"; + this.children = Collections.unmodifiableList(children); } } - - // "resource" is .class or any other resource (compressed/uncompressed) in a jimage. - // full path of the resource is the "name" of the resource. - static class Resource extends Node { + /** + * Resource node (e.g. a ".class" entry, or any other data resource). + * + *

Resources are leaf nodes referencing an underlying image location. They + * are lightweight, and do not cache their contents. + * + *

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(ImageLocation loc, BasicFileAttributes fileAttrs) { - super(loc.getFullName(true), fileAttrs); + private Resource(String name, ImageLocation loc, BasicFileAttributes fileAttrs) { + super(name, fileAttrs); this.loc = loc; } - static Resource create(Directory parent, ImageLocation loc, BasicFileAttributes fileAttrs) { - Resource rs = new Resource(loc, fileAttrs); - parent.addChild(rs); - return rs; - } - @Override - public boolean isCompleted() { - return true; + ImageLocation getLocation() { + return loc; } @Override @@ -799,11 +776,6 @@ public final class ImageReader implements AutoCloseable { return true; } - @Override - public ImageLocation getLocation() { - return loc; - } - @Override public long size() { return loc.getUncompressedSize(); @@ -818,36 +790,29 @@ public final class ImageReader implements AutoCloseable { public String extension() { return loc.getExtension(); } - - @Override - public long contentOffset() { - return loc.getContentOffset(); - } } - // represents a soft link to another Node - static class LinkNode extends Node { - private final Node link; + /** + * Link node (a symbolic link to a top-level modules directory). + * + *

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 link; - private LinkNode(String name, Node link) { - super(name, link.getFileAttributes()); + private LinkNode(String name, Supplier link, BasicFileAttributes fileAttrs) { + super(name, fileAttrs); this.link = link; } - static LinkNode create(Directory parent, String name, Node link) { - LinkNode ln = new LinkNode(name, link); - parent.addChild(ln); - return ln; - } - - @Override - public boolean isCompleted() { - return true; - } - @Override public Node resolveLink(boolean recursive) { - return (recursive && link instanceof LinkNode) ? ((LinkNode)link).resolveLink(true) : link; + // 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 diff --git a/src/java.base/share/classes/jdk/internal/jrtfs/ExplodedImage.java b/src/java.base/share/classes/jdk/internal/jrtfs/ExplodedImage.java index e2c17f8ca25..87a00da4393 100644 --- a/src/java.base/share/classes/jdk/internal/jrtfs/ExplodedImage.java +++ b/src/java.base/share/classes/jdk/internal/jrtfs/ExplodedImage.java @@ -119,9 +119,9 @@ class ExplodedImage extends SystemImage { } @Override - public List getChildren() { + public Stream getChildNames() { if (!isDirectory()) - throw new IllegalArgumentException("not a directory: " + getNameString()); + throw new IllegalArgumentException("not a directory: " + getName()); if (children == null) { List list = new ArrayList<>(); try (DirectoryStream stream = Files.newDirectoryStream(path)) { @@ -138,7 +138,7 @@ class ExplodedImage extends SystemImage { } children = list; } - return children; + return children.stream().map(Node::getName); } @Override diff --git a/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileAttributes.java b/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileAttributes.java index f0804b58c1c..e1eb1115260 100644 --- a/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileAttributes.java +++ b/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -49,7 +49,7 @@ final class JrtFileAttributes implements BasicFileAttributes { //-------- basic attributes -------- @Override public FileTime creationTime() { - return node.creationTime(); + return node.getFileAttributes().creationTime(); } @Override @@ -69,12 +69,12 @@ final class JrtFileAttributes implements BasicFileAttributes { @Override public FileTime lastAccessTime() { - return node.lastAccessTime(); + return node.getFileAttributes().lastAccessTime(); } @Override public FileTime lastModifiedTime() { - return node.lastModifiedTime(); + return node.getFileAttributes().lastModifiedTime(); } @Override 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 9a8d9d22dfa..6530bd1f90a 100644 --- a/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java +++ b/src/java.base/share/classes/jdk/internal/jrtfs/JrtFileSystem.java @@ -54,7 +54,6 @@ import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.UserPrincipalLookupService; import java.nio.file.spi.FileSystemProvider; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -64,7 +63,6 @@ import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; import jdk.internal.jimage.ImageReader.Node; -import static java.util.stream.Collectors.toList; /** * jrt file system implementation built on System jimage files. @@ -225,19 +223,19 @@ class JrtFileSystem extends FileSystem { throw new NotDirectoryException(path.getName()); } if (filter == null) { - return node.getChildren() - .stream() - .map(child -> (Path)(path.resolve(new JrtPath(this, child.getNameString()).getFileName()))) - .iterator(); + return node.getChildNames() + .map(child -> (Path) (path.resolve(new JrtPath(this, child).getFileName()))) + .iterator(); } - return node.getChildren() - .stream() - .map(child -> (Path)(path.resolve(new JrtPath(this, child.getNameString()).getFileName()))) - .filter(p -> { try { return filter.accept(p); - } catch (IOException x) {} - return false; - }) - .iterator(); + return node.getChildNames() + .map(child -> (Path) (path.resolve(new JrtPath(this, child).getFileName()))) + .filter(p -> { + try { + return filter.accept(p); + } catch (IOException x) {} + return false; + }) + .iterator(); } // returns the content of the file resource specified by the path 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 88cdf724e7d..6813c7e627f 100644 --- a/src/java.base/share/classes/jdk/internal/jrtfs/SystemImage.java +++ b/src/java.base/share/classes/jdk/internal/jrtfs/SystemImage.java @@ -58,7 +58,6 @@ abstract class SystemImage { if (modulesImageExists) { // open a .jimage and build directory structure final ImageReader image = ImageReader.open(moduleImageFile); - image.getRootDirectory(); return new SystemImage() { @Override Node findNode(String path) throws IOException { 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 c520e6e636a..09ad3dc456d 100644 --- a/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java +++ b/src/java.base/share/classes/jdk/internal/module/SystemModuleFinders.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,7 +34,6 @@ import java.lang.module.ModuleReader; import java.lang.module.ModuleReference; import java.lang.reflect.Constructor; import java.net.URI; -import java.net.URLConnection; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; @@ -54,7 +53,6 @@ import java.util.function.Supplier; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import jdk.internal.jimage.ImageLocation; import jdk.internal.jimage.ImageReader; import jdk.internal.jimage.ImageReaderFactory; import jdk.internal.access.JavaNetUriAccess; @@ -210,7 +208,7 @@ public final class SystemModuleFinders { } /** - * Parses the module-info.class of all module in the runtime image and + * Parses the {@code module-info.class} of all modules in the runtime image and * returns a ModuleFinder to find the modules. * * @apiNote The returned ModuleFinder is thread safe. @@ -219,20 +217,16 @@ public final class SystemModuleFinders { // parse the module-info.class in every module Map nameToAttributes = new HashMap<>(); Map nameToHash = new HashMap<>(); - ImageReader reader = SystemImage.reader(); - for (String mn : reader.getModuleNames()) { - ImageLocation loc = reader.findLocation(mn, "module-info.class"); - ModuleInfo.Attributes attrs - = ModuleInfo.read(reader.getResourceBuffer(loc), null); - nameToAttributes.put(mn, attrs); + allModuleAttributes().forEach(attrs -> { + nameToAttributes.put(attrs.descriptor().name(), attrs); ModuleHashes hashes = attrs.recordedHashes(); if (hashes != null) { for (String name : hashes.names()) { nameToHash.computeIfAbsent(name, k -> hashes.hashFor(name)); } } - } + }); // create a ModuleReference for each module Set mrefs = new HashSet<>(); @@ -253,6 +247,40 @@ public final class SystemModuleFinders { return new SystemModuleFinder(mrefs, nameToModule); } + /** + * Parses the {@code module-info.class} of all modules in the runtime image and + * returns a stream of {@link ModuleInfo.Attributes Attributes} for them. The + * returned attributes are in no specific order. + */ + private static Stream allModuleAttributes() { + // System-wide image reader. + ImageReader reader = SystemImage.reader(); + try { + return reader.findNode("/modules") + .getChildNames() + .map(mn -> readModuleAttributes(reader, mn)); + } catch (IOException e) { + throw new Error("Error reading root /modules entry", e); + } + } + + /** + * Returns the module's "module-info", returning a holder for its class file + * attributes. Every module is required to have a valid {@code module-info.class}. + */ + private static ModuleInfo.Attributes readModuleAttributes(ImageReader reader, String moduleName) { + Exception err = null; + try { + ImageReader.Node node = reader.findNode(moduleName + "/module-info.class"); + if (node != null && node.isResource()) { + return ModuleInfo.read(reader.getResourceBuffer(node), null); + } + } catch (IOException | UncheckedIOException e) { + err = e; + } + throw new Error("Missing or invalid module-info.class for module: " + moduleName, err); + } + /** * A ModuleFinder that finds module in an array or set of modules. */ @@ -382,34 +410,18 @@ public final class SystemModuleFinders { this.module = module; } - /** - * Returns the ImageLocation for the given resource, {@code null} - * if not found. - */ - private ImageLocation findImageLocation(String name) throws IOException { - Objects.requireNonNull(name); - if (closed) - throw new IOException("ModuleReader is closed"); - ImageReader imageReader = SystemImage.reader(); - if (imageReader != null) { - return imageReader.findLocation(module, name); - } else { - // not an images build - return null; - } - } - /** * Returns {@code true} if the given resource exists, {@code false} * if not found. */ - private boolean containsImageLocation(String name) throws IOException { - Objects.requireNonNull(name); + private boolean containsResource(String resourcePath) throws IOException { + Objects.requireNonNull(resourcePath); if (closed) throw new IOException("ModuleReader is closed"); ImageReader imageReader = SystemImage.reader(); if (imageReader != null) { - return imageReader.verifyLocation(module, name); + ImageReader.Node node = imageReader.findNode("/modules" + resourcePath); + return node != null && node.isResource(); } else { // not an images build return false; @@ -418,8 +430,9 @@ public final class SystemModuleFinders { @Override public Optional find(String name) throws IOException { - if (containsImageLocation(name)) { - URI u = JNUA.create("jrt", "/" + module + "/" + name); + String resourcePath = "/" + module + "/" + name; + if (containsResource(resourcePath)) { + URI u = JNUA.create("jrt", resourcePath); return Optional.of(u); } else { return Optional.empty(); @@ -442,14 +455,25 @@ public final class SystemModuleFinders { } } + /** + * Returns the node for the given resource if found. If the name references + * a non-resource node, then {@code null} is returned. + */ + private ImageReader.Node findResource(ImageReader reader, String name) throws IOException { + Objects.requireNonNull(name); + if (closed) { + throw new IOException("ModuleReader is closed"); + } + String nodeName = "/modules/" + module + "/" + name; + ImageReader.Node node = reader.findNode(nodeName); + return (node != null && node.isResource()) ? node : null; + } + @Override public Optional read(String name) throws IOException { - ImageLocation location = findImageLocation(name); - if (location != null) { - return Optional.of(SystemImage.reader().getResourceBuffer(location)); - } else { - return Optional.empty(); - } + ImageReader reader = SystemImage.reader(); + return Optional.ofNullable(findResource(reader, name)) + .map(reader::getResourceBuffer); } @Override @@ -481,7 +505,7 @@ public final class SystemModuleFinders { private static class ModuleContentSpliterator implements Spliterator { final String moduleRoot; final Deque stack; - Iterator iterator; + Iterator iterator; ModuleContentSpliterator(String module) throws IOException { moduleRoot = "/modules/" + module; @@ -502,13 +526,10 @@ public final class SystemModuleFinders { private String next() throws IOException { for (;;) { while (iterator.hasNext()) { - ImageReader.Node node = iterator.next(); - String name = node.getName(); + String name = iterator.next(); + ImageReader.Node node = SystemImage.reader().findNode(name); if (node.isDirectory()) { - // build node - ImageReader.Node dir = SystemImage.reader().findNode(name); - assert dir.isDirectory(); - stack.push(dir); + stack.push(node); } else { // strip /modules/$MODULE/ prefix return name.substring(moduleRoot.length() + 1); @@ -520,7 +541,7 @@ public final class SystemModuleFinders { } else { ImageReader.Node dir = stack.poll(); assert dir.isDirectory(); - iterator = dir.getChildren().iterator(); + iterator = dir.getChildNames().iterator(); } } } diff --git a/test/jdk/jdk/internal/jimage/ImageReaderTest.java b/test/jdk/jdk/internal/jimage/ImageReaderTest.java new file mode 100644 index 00000000000..6bdf0cf479a --- /dev/null +++ b/test/jdk/jdk/internal/jimage/ImageReaderTest.java @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import jdk.internal.jimage.ImageReader; +import jdk.internal.jimage.ImageReader.Node; +import jdk.test.lib.compiler.InMemoryJavaCompiler; +import jdk.test.lib.util.JarBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentest4j.TestSkippedException; +import tests.Helper; +import tests.JImageGenerator; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +/* + * @test + * @summary Tests for ImageReader. + * @modules java.base/jdk.internal.jimage + * jdk.jlink/jdk.tools.jimage + * @library /test/jdk/tools/lib + * /test/lib + * @build tests.* + * @run junit/othervm ImageReaderTest + */ + +/// Using PER_CLASS lifecycle means the (expensive) image file is only build once. +/// There is no mutable test instance state to worry about. +@TestInstance(PER_CLASS) +public class ImageReaderTest { + + private static final Map> IMAGE_ENTRIES = Map.of( + "modfoo", Arrays.asList( + "com.foo.Alpha", + "com.foo.Beta", + "com.foo.bar.Gamma"), + "modbar", Arrays.asList( + "com.bar.One", + "com.bar.Two")); + private final Path image = buildJImage(IMAGE_ENTRIES); + + @ParameterizedTest + @ValueSource(strings = { + "/", + "/modules", + "/modules/modfoo", + "/modules/modbar", + "/modules/modfoo/com", + "/modules/modfoo/com/foo", + "/modules/modfoo/com/foo/bar"}) + public void testModuleDirectories_expected(String name) throws IOException { + try (ImageReader reader = ImageReader.open(image)) { + assertDir(reader, name); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "", + "//", + "/modules/", + "/modules/unknown", + "/modules/modbar/", + "/modules/modfoo//com", + "/modules/modfoo/com/"}) + public void testModuleNodes_absent(String name) throws IOException { + try (ImageReader reader = ImageReader.open(image)) { + assertAbsent(reader, name); + } + } + + @Test + public void testModuleResources() throws IOException { + try (ImageReader reader = ImageReader.open(image)) { + assertNode(reader, "/modules/modfoo/com/foo/Alpha.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")); + } + } + + @Test + public void testPackageDirectories() throws IOException { + try (ImageReader reader = ImageReader.open(image)) { + Node root = assertDir(reader, "/packages"); + Set pkgNames = root.getChildNames().collect(Collectors.toSet()); + assertTrue(pkgNames.contains("/packages/com")); + assertTrue(pkgNames.contains("/packages/com.foo")); + assertTrue(pkgNames.contains("/packages/com.bar")); + + // Even though no classes exist directly in the "com" package, it still + // creates a directory with links back to all the modules which contain it. + Set comLinks = assertDir(reader, "/packages/com").getChildNames().collect(Collectors.toSet()); + assertTrue(comLinks.contains("/packages/com/modfoo")); + assertTrue(comLinks.contains("/packages/com/modbar")); + } + } + + @Test + public void testPackageLinks() throws IOException { + try (ImageReader reader = ImageReader.open(image)) { + Node moduleFoo = assertDir(reader, "/modules/modfoo"); + Node moduleBar = assertDir(reader, "/modules/modbar"); + assertSame(assertLink(reader, "/packages/com.foo/modfoo").resolveLink(), moduleFoo); + assertSame(assertLink(reader, "/packages/com.bar/modbar").resolveLink(), moduleBar); + } + } + + private static ImageReader.Node assertNode(ImageReader reader, String name) throws IOException { + ImageReader.Node node = reader.findNode(name); + assertNotNull(node, "Could not find node: " + name); + return node; + } + + private static ImageReader.Node assertDir(ImageReader reader, String name) throws IOException { + ImageReader.Node dir = assertNode(reader, name); + assertTrue(dir.isDirectory(), "Node was not a directory: " + name); + return dir; + } + + 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); + return link; + } + + private static void assertAbsent(ImageReader reader, String name) throws IOException { + assertNull(reader.findNode(name), "Should not be able to find node: " + name); + } + + /// 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. + public static Path buildJImage(Map> entries) { + Helper helper = getHelper(); + Path outDir = helper.createNewImageDir("test"); + JImageGenerator.JLinkTask jlink = JImageGenerator.getJLinkTask() + .modulePath(helper.defaultModulePath()) + .output(outDir); + + Path jarDir = helper.getJarDir(); + entries.forEach((module, classes) -> { + JarBuilder jar = new JarBuilder(jarDir.resolve(module + ".jar").toString()); + String moduleInfo = "module " + module + " {}"; + jar.addEntry("module-info.class", InMemoryJavaCompiler.compile("module-info", moduleInfo)); + + classes.forEach(fqn -> { + 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"; + } + } + """, pkg, cls, fqn); + jar.addEntry(path, InMemoryJavaCompiler.compile(fqn, source)); + }); + try { + jar.build(); + } catch (IOException e) { + throw new RuntimeException(e); + } + jlink.addMods(module); + }); + return jlink.call().assertSuccess().resolve("lib", "modules"); + } + + /// Returns the helper for building JAR and jimage files. + private static Helper getHelper() { + try { + Helper helper = Helper.newHelper(); + if (helper == null) { + throw new TestSkippedException("Cannot create test helper (exploded image?)"); + } + return helper; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /// Loads and performs actions on classes stored in a given `ImageReader`. + private static class ImageClassLoader extends ClassLoader { + private final ImageReader reader; + private final Set testModules; + + private ImageClassLoader(ImageReader reader, Set testModules) { + this.reader = reader; + this.testModules = testModules; + } + + @FunctionalInterface + public interface ClassAction { + R call(Class cls) throws T; + } + + String loadAndGetToString(String module, String fqn) { + return loadAndCall(module, fqn, c -> c.getDeclaredConstructor().newInstance().toString()); + } + + R loadAndCall(String module, String fqn, ClassAction action) { + Class cls = findClass(module, fqn); + assertNotNull(cls, "Could not load class: " + module + "/" + fqn); + try { + return action.call(cls); + } catch (Exception e) { + fail("Class loading failed", e); + return null; + } + } + + @Override + protected Class findClass(String module, String fqn) { + assumeTrue(testModules.contains(module), "Can only load classes in modules: " + testModules); + String name = "/modules/" + module + "/" + fqn.replace('.', '/') + ".class"; + Class cls = findLoadedClass(fqn); + if (cls == null) { + try { + ImageReader.Node node = reader.findNode(name); + if (node != null && node.isResource()) { + byte[] classBytes = reader.getResource(node); + cls = defineClass(fqn, classBytes, 0, classBytes.length); + resolveClass(cls); + return cls; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return null; + } + } +} diff --git a/test/jdk/jdk/internal/jimage/JImageReadTest.java b/test/jdk/jdk/internal/jimage/JImageReadTest.java index ea700d03a4f..35fb2adb687 100644 --- a/test/jdk/jdk/internal/jimage/JImageReadTest.java +++ b/test/jdk/jdk/internal/jimage/JImageReadTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -49,6 +49,9 @@ import org.testng.annotations.Test; import org.testng.Assert; import org.testng.TestNG; +import static java.nio.ByteOrder.BIG_ENDIAN; +import static java.nio.ByteOrder.LITTLE_ENDIAN; + @Test public class JImageReadTest { @@ -333,32 +336,21 @@ public class JImageReadTest { */ @Test static void test5_imageReaderEndianness() throws IOException { - ImageReader nativeReader = ImageReader.open(imageFile); - Assert.assertEquals(nativeReader.getByteOrder(), ByteOrder.nativeOrder()); - - try { - ImageReader leReader = ImageReader.open(imageFile, ByteOrder.LITTLE_ENDIAN); - Assert.assertEquals(leReader.getByteOrder(), ByteOrder.LITTLE_ENDIAN); - leReader.close(); - } catch (IOException io) { - // IOException expected if LITTLE_ENDIAN not the nativeOrder() - Assert.assertNotEquals(ByteOrder.nativeOrder(), ByteOrder.LITTLE_ENDIAN); + // Will be opened with native byte order. + try (ImageReader nativeReader = ImageReader.open(imageFile)) { + // Just ensure something works as expected. + Assert.assertNotNull(nativeReader.findNode("/")); + } catch (IOException expected) { + Assert.fail("Reader should be openable with native byte order."); } - try { - ImageReader beReader = ImageReader.open(imageFile, ByteOrder.BIG_ENDIAN); - Assert.assertEquals(beReader.getByteOrder(), ByteOrder.BIG_ENDIAN); - beReader.close(); - } catch (IOException io) { - // IOException expected if LITTLE_ENDIAN not the nativeOrder() - Assert.assertNotEquals(ByteOrder.nativeOrder(), ByteOrder.BIG_ENDIAN); - } - - nativeReader.close(); + // 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)); } - // main method to run standalone from jtreg - @Test(enabled=false) + // main method to run standalone from jtreg + @Test(enabled = false) @Parameters({"x"}) @SuppressWarnings("raw_types") public static void main(@Optional String[] args) { diff --git a/test/jdk/tools/jimage/ImageReaderDuplicateChildNodesTest.java b/test/jdk/tools/jimage/ImageReaderDuplicateChildNodesTest.java index bec32bee0f8..8656a4a3d00 100644 --- a/test/jdk/tools/jimage/ImageReaderDuplicateChildNodesTest.java +++ b/test/jdk/tools/jimage/ImageReaderDuplicateChildNodesTest.java @@ -68,17 +68,17 @@ public class ImageReaderDuplicateChildNodesTest { + " in " + imagePath); } // now verify that the parent node which is a directory, doesn't have duplicate children - final List children = parent.getChildren(); - if (children == null || children.isEmpty()) { + final List childNames = parent.getChildNames().toList(); + if (childNames.isEmpty()) { throw new RuntimeException("ImageReader did not return any child resources under " + integersParentResource + " in " + imagePath); } final Set uniqueChildren = new HashSet<>(); - for (final ImageReader.Node child : children) { - final boolean unique = uniqueChildren.add(child); + for (final String childName : childNames) { + final boolean unique = uniqueChildren.add(reader.findNode(childName)); if (!unique) { throw new RuntimeException("ImageReader returned duplicate child resource " - + child + " under " + parent + " from image " + imagePath); + + childName + " under " + parent + " from image " + imagePath); } } } 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 c58a4dc33b2..4f97e12171f 100644 --- a/test/micro/org/openjdk/bench/jdk/internal/jrtfs/ImageReaderBenchmark.java +++ b/test/micro/org/openjdk/bench/jdk/internal/jrtfs/ImageReaderBenchmark.java @@ -195,9 +195,9 @@ public class ImageReaderBenchmark { static long countAllNodes(ImageReader reader, Node node) { long count = 1; if (node.isDirectory()) { - count += node.getChildren().stream().mapToLong(n -> { + count += node.getChildNames().mapToLong(n -> { try { - return countAllNodes(reader, reader.findNode(n.getName())); + return countAllNodes(reader, reader.findNode(n)); } catch (IOException e) { throw new RuntimeException(e); }