< prev index next > src/java.base/share/classes/jdk/internal/jimage/ImageReader.java
Print this page
/*
! * Copyright (c) 2014, 2022, 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
/*
! * 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
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* questions.
*/
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.util.ArrayList;
import java.util.Collections;
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.function.Consumer;
/**
* @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.
* questions.
*/
package jdk.internal.jimage;
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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
! 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.
+ *
+ * <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.
private ImageReader(SharedImageReader reader) {
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);
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());
}
@Override
if (closed) {
throw new IllegalStateException("image file closed");
}
}
! // directory management interface
! public Directory getRootDirectory() throws IOException {
! ensureOpen();
! return reader.getRootDirectory();
! }
!
!
public Node findNode(String name) throws IOException {
ensureOpen();
return reader.findNode(name);
}
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();
- }
-
public static void releaseByteBuffer(ByteBuffer buffer) {
BasicImageReader.releaseByteBuffer(buffer);
}
! public String getName() {
! 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);
}
private static final class SharedImageReader extends BasicImageReader {
! static final int SIZE_OF_OFFSET = Integer.BYTES;
!
! static final Map<Path, SharedImageReader> OPEN_FILES = new HashMap<>();
// List of openers for this shared image.
! final Set<ImageReader> openers;
! // attributes of the .jimage file. 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;
-
- // directory management implementation
- final HashMap<String, Node> nodes;
- volatile Directory rootDir;
! Directory packagesDir;
! Directory modulesDir;
private SharedImageReader(Path imagePath, ByteOrder byteOrder) throws IOException {
super(imagePath, byteOrder);
! this.openers = new HashSet<>();
! this.nodes = new HashMap<>();
}
! public static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException {
Objects.requireNonNull(imagePath);
Objects.requireNonNull(byteOrder);
synchronized (OPEN_FILES) {
SharedImageReader reader = OPEN_FILES.get(imagePath);
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 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);
}
! /**
! * Releases a (possibly cached) {@link ByteBuffer} obtained via
! * {@link #getResourceBuffer(Node)}.
! *
! * <p>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);
}
! /**
! * 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();
! if (!node.isResource()) {
! throw new IllegalArgumentException("Not a resource node: " + node);
! }
! return reader.getResourceBuffer(node.getLocation());
}
private static final class SharedImageReader extends BasicImageReader {
! private static final Map<Path, SharedImageReader> OPEN_FILES = new HashMap<>();
! private static final String MODULES_ROOT = "/modules";
! private static final String PACKAGES_ROOT = "/packages";
+ // There are >30,000 nodes in a complete jimage tree, and even relatively
+ // common tasks (e.g. starting up javac) load somewhere in the region of
+ // 1000 classes. Thus, an initial capacity of 2000 is a reasonable guess.
+ private static final int INITIAL_NODE_CACHE_CAPACITY = 2000;
// 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;
+ // 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.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);
+ }
+
+ /**
+ * Returns the offset of the string denoting the leading "module" segment in
+ * the given path (e.g. {@code <module>/<path>}). We can't just pass in the
+ * {@code /<module>} string here because that has a module offset of zero.
+ */
+ 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);
synchronized (OPEN_FILES) {
SharedImageReader reader = OPEN_FILES.get(imagePath);
}
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");
}
}
}
}
- void addOpener(ImageReader reader) {
- synchronized (OPEN_FILES) {
- openers.add(reader);
- }
- }
-
- boolean removeOpener(ImageReader reader) {
- synchronized (OPEN_FILES) {
- return openers.remove(reader);
- }
- }
-
- // directory management interface
- Directory getRootDirectory() {
- return buildRootDirectory();
- }
-
/**
! * Lazily build a node from a name.
*/
! synchronized Node buildNode(String name) {
! Node n;
! boolean isPackages = name.startsWith("/packages");
! boolean isModules = !isPackages && name.startsWith("/modules");
!
! if (!(isModules || isPackages)) {
! return null;
! }
!
- 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);
}
}
! 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;
}
/**
! * To visit sub tree resources.
*/
! 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;
} 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;
- }
- }
- }
- return n;
- }
-
- // Asking for /packages/package/module although
- // /packages/<pkg>/ 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;
- }
}
}
}
! return ret;
- }
-
- 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);
- }
- }
- });
- 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);
! 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);
! }
- return makeDirectory(parent, last);
}
! Directory makeDirectory(String dir, Directory last) {
! Directory nextDir = (Directory) nodes.get(dir);
! if (nextDir == null) {
! nextDir = newDirectory(last, dir);
! }
! return nextDir;
}
! byte[] getResource(Node node) throws IOException {
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
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;
-
- 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.
*
! * @return
*/
! 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;
}
public final BasicFileAttributes getFileAttributes() {
return fileAttrs;
}
! // resolve this Node (if this is a soft link, get underlying Node)
public final Node resolveLink() {
return resolveLink(false);
}
public Node resolveLink(boolean recursive) {
return this;
}
! // is this a soft link Node?
public boolean isLink() {
return false;
}
public boolean isDirectory() {
return false;
}
! public List<Node> getChildren() {
! throw new IllegalArgumentException("not a directory: " + getNameString());
! }
!
public boolean isResource() {
return false;
}
! public ImageLocation getLocation() {
! throw new IllegalArgumentException("not a resource: " + getNameString());
}
public long size() {
return 0L;
}
public long compressedSize() {
return 0L;
}
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();
}
@Override
public final int hashCode() {
return name.hashCode();
}
@Override
public final boolean equals(Object other) {
if (this == other) {
return true;
}
}
if (openers.isEmpty()) {
close();
nodes.clear();
if (!OPEN_FILES.remove(this.getImagePath(), 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>This is the only public API by which anything outside this class can access
+ * {@code Node} instances either directly, or by resolving symbolic links.
+ *
+ * <p>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);
}
! assert node == null || node.isCompleted() : "Incomplete node: " + node;
! return node;
}
/**
! * 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 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) {
! assert name.equals(loc.getFullName()) : "Mismatched location for directory: " + name;
! assert isModulesSubdirectory(loc) : "Invalid modules directory: " + name;
! return completeModuleDirectory(newDirectory(name), loc);
}
+ // Now try the non-prefixed resource name, but be careful to avoid false
+ // positives for names like "/modules/modules/xxx" which could return a
+ // location of a directory entry.
+ loc = findLocation(name.substring(MODULES_ROOT.length()));
+ return loc != null && isResource(loc) ? newResource(name, loc) : null;
}
! /**
! * Builds a node in the "/packages/..." namespace.
! *
! * <p>Called by {@link #findNode(String)} if a {@code /packages/...} node
! * is not present in the cache.
! */
! 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 {
! // 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 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_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);
}
! assert dir.isCompleted() : "Directory must be complete by now: " + dir;
}
! /**
! * Completes a modules directory by setting the list of child nodes.
! *
! * <p>The given directory can be the top level {@code /modules} directory,
! * so it is NOT safe to use {@code isModulesSubdirectory(loc)} here.
! */
! private Directory completeModuleDirectory(Directory dir, ImageLocation loc) {
+ assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir;
+ List<Node> children = createChildNodes(loc, childLoc -> {
+ if (isModulesSubdirectory(childLoc)) {
+ return nodes.computeIfAbsent(childLoc.getFullName(), this::newDirectory);
+ } 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.setChildren(children);
+ return dir;
}
! /**
! * Completes a package directory by setting the list of child nodes.
! *
! * <p>The given directory can be the top level {@code /packages} directory,
! * so it is NOT safe to use {@code isPackagesSubdirectory(loc)} here.
+ */
+ private Directory completePackageDirectory(Directory dir, ImageLocation loc) {
+ assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir;
+ // The only directories in the "/packages" namespace are "/packages" or
+ // "/packages/<package>". However, unlike "/modules" directories, the
+ // location offsets mean different things.
+ List<Node> children;
+ if (dir.getName().equals(PACKAGES_ROOT)) {
+ // Top-level directory just contains a list of subdirectories.
+ children = createChildNodes(loc, c -> nodes.computeIfAbsent(c.getFullName(), this::newDirectory));
+ } else {
+ // A package directory's content is array of offset PAIRS in the
+ // Strings table, but we only need the 2nd value of each pair.
+ IntBuffer intBuffer = getOffsetBuffer(loc);
+ int offsetCount = intBuffer.capacity();
+ assert (offsetCount & 0x1) == 0 : "Offset count must be even: " + offsetCount;
+ children = new ArrayList<>(offsetCount / 2);
+ // Iterate the 2nd offset in each pair (odd indices).
+ for (int i = 1; i < offsetCount; i += 2) {
+ String moduleName = getString(intBuffer.get(i));
+ children.add(nodes.computeIfAbsent(
+ dir.getName() + "/" + moduleName,
+ n -> newLinkNode(n, MODULES_ROOT + "/" + moduleName)));
+ }
}
! // This only happens once and "completes" the directory.
+ dir.setChildren(children);
+ return dir;
}
/**
! * Creates the list of child nodes for a {@code Directory} based on a given
+ *
+ * <p>Note: This cannot be used for package subdirectories as they have
+ * child offsets stored differently to other directories.
*/
! private List<Node> createChildNodes(ImageLocation loc, Function<ImageLocation, Node> newChildFn) {
! IntBuffer offsets = getOffsetBuffer(loc);
! int childCount = offsets.capacity();
! List<Node> children = new ArrayList<>(childCount);
! for (int i = 0; i < childCount; i++) {
! children.add(newChildFn.apply(getLocation(offsets.get(i))));
}
! return children;
}
! /** Helper to extract the integer offset buffer from a directory location. */
! private IntBuffer getOffsetBuffer(ImageLocation dir) {
! assert !isResource(dir) : "Not a directory: " + dir.getFullName();
! byte[] offsets = getResource(dir);
+ ByteBuffer buffer = ByteBuffer.wrap(offsets);
+ buffer.order(getByteOrder());
+ return buffer.asIntBuffer();
}
! /**
! * Efficiently determines if an image location is a resource.
! *
! * <p>A resource must have a valid module associated with it, so its
+ * module offset must be non-zero, and not equal to the offsets for
+ * "/modules/..." or "/packages/..." entries.
+ */
+ private boolean isResource(ImageLocation loc) {
+ int moduleOffset = loc.getModuleOffset();
+ return moduleOffset != 0
+ && moduleOffset != modulesStringOffset
+ && moduleOffset != packagesStringOffset;
}
! /**
! * Determines if an image location is a directory in the {@code /modules}
! * namespace (if so, the location name is the node name).
! *
+ * <p>In jimage, every {@code ImageLocation} under {@code /modules/} is a
+ * directory and has the same value for {@code getModule()}, and {@code
+ * getModuleOffset()}.
+ */
+ private boolean isModulesSubdirectory(ImageLocation loc) {
+ return loc.getModuleOffset() == modulesStringOffset;
}
! /**
! * Creates an "incomplete" directory node with no child nodes set.
! * Directories need to be "completed" before they are returned by
! * {@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.
! *
! * <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;
}
return false;
}
}
! // directory node - directory has full path name without '/' at end.
! static final class Directory extends Node {
! private final List<Node> children;
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
public boolean isDirectory() {
return true;
}
@Override
! public List<Node> getChildren() {
! return Collections.unmodifiableList(children);
! }
-
- void addChild(Node node) {
- assert !children.contains(node) : "Child " + node + " already added";
- children.add(node);
- }
-
- public void walk(Consumer<? super Node> consumer) {
- consumer.accept(this);
- for (Node child : children) {
- if (child.isDirectory()) {
- ((Directory)child).walk(consumer);
- } else {
- consumer.accept(child);
- }
}
! }
! }
!
! // "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 {
private final ImageLocation loc;
! private Resource(ImageLocation loc, BasicFileAttributes fileAttrs) {
! super(loc.getFullName(true), 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;
}
@Override
public boolean isResource() {
return true;
}
- @Override
- public ImageLocation getLocation() {
- return loc;
- }
-
@Override
public long size() {
return loc.getUncompressedSize();
}
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<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 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;
! private LinkNode(String name, Node link) {
! super(name, link.getFileAttributes());
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;
}
@Override
public boolean isLink() {
return true;
@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;
< prev index next >