< prev index next >

src/java.base/share/classes/jdk/internal/jimage/ImageReader.java

Print this page
@@ -1,7 +1,7 @@
  /*
-  * 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
   * under the terms of the GNU General Public License version 2 only, as
   * published by the Free Software Foundation.  Oracle designates this

@@ -23,30 +23,55 @@
   * 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.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.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.
+  *
+  * <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.

@@ -58,17 +83,25 @@
  
      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

@@ -90,150 +123,117 @@
          if (closed) {
              throw new IllegalStateException("image file closed");
          }
      }
  
-     // 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/<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);
      }
  
-     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)}.
+      *
+      * <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);
      }
  
-     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) {
+     /**
+      * 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.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<Path, SharedImageReader> OPEN_FILES = new HashMap<>();
+         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.
-         final Set<ImageReader> openers;
+         private final Set<ImageReader> 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;
- 
-         // directory management implementation
-         final HashMap<String, Node> nodes;
-         volatile Directory rootDir;
+         private final BasicFileAttributes imageFileAttributes;
  
-         Directory packagesDir;
-         Directory modulesDir;
+         // 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.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);
+         }
+ 
+         /**
+          * 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;
          }
  
-         public static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException {
+         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);

@@ -262,461 +262,424 @@
                  }
  
                  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.
+          * 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 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);
+         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);
                  }
-             } 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 (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);
              }
-             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;
+             assert node == null || node.isCompleted() : "Incomplete node: " + node;
+             return node;
          }
  
          /**
-          * To visit sub tree resources.
+          * Builds a node in the "/modules/..." namespace.
+          *
+          * <p>Called by {@link #findNode(String)} if a {@code /modules/...} 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);
-                 }
+         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;
          }
  
-         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;
+         /**
+          * 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 {
-                 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;
-                         }
+                 // 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 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;
+             return null;
          }
  
-         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;
+         /** 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 n;
+             assert dir.isCompleted() : "Directory must be complete by now: " + dir;
          }
  
-         String getBaseExt(ImageLocation loc) {
-             String base = loc.getBase();
-             String ext = loc.getExtension();
-             if (ext != null && !ext.isEmpty()) {
-                 base = base + "." + ext;
-             }
-             return base;
+         /**
+          * 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;
          }
  
-         synchronized Node findNode(String name) {
-             buildRootDirectory();
-             Node n = nodes.get(name);
-             if (n == null || !n.isCompleted()) {
-                 n = buildNode(name);
+         /**
+          * 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)));
+                 }
              }
-             return n;
+             // This only happens once and "completes" the directory.
+             dir.setChildren(children);
+             return dir;
          }
  
          /**
-          * Returns the file attributes of the image file.
+          * 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.
           */
-         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;
+         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 attrs;
+             return children;
          }
  
-         Directory newDirectory(Directory parent, String name) {
-             Directory dir = Directory.create(parent, name, imageFileAttributes());
-             nodes.put(dir.getName(), dir);
-             return dir;
+         /** 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();
          }
  
-         Resource newResource(Directory parent, ImageLocation loc) {
-             Resource res = Resource.create(parent, loc, imageFileAttributes());
-             nodes.put(res.getName(), res);
-             return res;
+         /**
+          * 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;
          }
  
-         LinkNode newLinkNode(Directory dir, String name, Node link) {
-             LinkNode linkNode = LinkNode.create(dir, name, link);
-             nodes.put(linkNode.getName(), linkNode);
-             return linkNode;
+         /**
+          * 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;
          }
  
-         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);
+         /**
+          * 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);
          }
  
-         Directory makeDirectory(String dir, Directory last) {
-             Directory nextDir = (Directory) nodes.get(dir);
-             if (nextDir == null) {
-                 nextDir = newDirectory(last, dir);
-             }
-             return nextDir;
+         /**
+          * 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);
          }
  
-         byte[] getResource(Node node) throws IOException {
+         /** 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.
+      *
+      * <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 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.
+          * Creates an abstract {@code Node}, which is either a resource, directory
+          * or symbolic link.
           *
-          * @return
+          * <p>This constructor is only non-private so it can be used by the
+          * {@code ExplodedImage} class, and must not be used otherwise.
           */
-         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;
+         protected Node(String name, BasicFileAttributes fileAttrs) {
+             this.name = Objects.requireNonNull(name);
+             this.fileAttrs = Objects.requireNonNull(fileAttrs);
          }
  
-         public final void setIsModulesDir() {
-             flags |= MODULES_DIR;
+         // A node is completed when all its direct children have been built.
+         // As such, non-directory nodes are always complete.
+         boolean isCompleted() {
+             return true;
          }
  
-         public final boolean isModulesDir() {
-             return (flags & MODULES_DIR) != 0;
+         // 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;
          }
  
-         // 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<Node> 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.
+          *
+          * <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;
          }
  
-         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 <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;
              }

@@ -727,85 +690,94 @@
  
              return false;
          }
      }
  
-     // directory node - directory has full path name without '/' at end.
-     static final class Directory extends Node {
-         private final List<Node> children;
+     /**
+      * 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);
-             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
          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);
-                 }
+         public Stream<String> getChildNames() {
+             if (children != null) {
+                 return children.stream().map(Node::getName);
              }
-         }
-     }
- 
-     // "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 {
+             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(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
          public boolean isResource() {
              return true;
          }
  
-         @Override
-         public ImageLocation getLocation() {
-             return loc;
-         }
- 
          @Override
          public long size() {
              return loc.getUncompressedSize();
          }
  

@@ -816,40 +788,33 @@
  
          @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;
+     /**
+      * 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, Node link) {
-             super(name, link.getFileAttributes());
+         private LinkNode(String name, Supplier<Node> 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
          public boolean isLink() {
              return true;
< prev index next >