1 /*
   2  * Copyright (c) 2014, 2026, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package jdk.internal.jimage;
  26 
  27 import jdk.internal.jimage.ImageLocation.LocationType;
  28 
  29 import java.io.IOException;
  30 import java.nio.ByteBuffer;
  31 import java.nio.ByteOrder;
  32 import java.nio.IntBuffer;
  33 import java.nio.file.Files;
  34 import java.nio.file.Path;
  35 import java.nio.file.attribute.BasicFileAttributes;
  36 import java.util.ArrayList;
  37 import java.util.Arrays;
  38 import java.util.Collections;
  39 import java.util.Comparator;
  40 import java.util.HashMap;
  41 import java.util.HashSet;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.Objects;
  45 import java.util.Set;
  46 import java.util.TreeMap;
  47 import java.util.function.Function;
  48 import java.util.function.Supplier;
  49 import java.util.stream.Stream;
  50 
  51 import static jdk.internal.jimage.ImageLocation.LocationType.MODULES_DIR;
  52 import static jdk.internal.jimage.ImageLocation.LocationType.MODULES_ROOT;
  53 import static jdk.internal.jimage.ImageLocation.LocationType.PACKAGES_DIR;
  54 import static jdk.internal.jimage.ImageLocation.LocationType.RESOURCE;
  55 import static jdk.internal.jimage.ImageLocation.MODULES_PREFIX;
  56 import static jdk.internal.jimage.ImageLocation.PACKAGES_PREFIX;
  57 import static jdk.internal.jimage.ImageLocation.PREVIEW_INFIX;
  58 
  59 /**
  60  * A view over the entries of a jimage file with a unified namespace suitable
  61  * for file system use. The jimage entries (resources, module and package
  62  * information) are mapped into a unified hierarchy of named nodes, which serve
  63  * as the underlying structure for {@code JrtFileSystem} and other utilities.
  64  *
  65  * <p>Entries in jimage are expressed as one of three {@link Node} types;
  66  * resource nodes, directory nodes and link nodes.
  67  *
  68  * <p>When remapping jimage entries, jimage location names (e.g. {@code
  69  * "/java.base/java/lang/Integer.class"}) are prefixed with {@code "/modules"}
  70  * to form the names of resource nodes. This aligns with the naming of module
  71  * entries in jimage (e.g. "/modules/java.base/java/lang"), which appear as
  72  * directory nodes in {@code ImageReader}.
  73  *
  74  * <p>Package entries (e.g. {@code "/packages/java.lang"} appear as directory
  75  * nodes containing link nodes, which resolve back to the root directory of the
  76  * module in which that package exists (e.g. {@code "/modules/java.base"}).
  77  * Unlike other nodes, the jimage file does not contain explicit entries for
  78  * link nodes, and their existence is derived only from the contents of the
  79  * parent directory.
  80  *
  81  * <p>While similar to {@code BasicImageReader}, this class is not a conceptual
  82  * subtype of it, and deliberately hides types such as {@code ImageLocation} to
  83  * give a focused API based only on nodes.
  84  *
  85  * @implNote This class needs to maintain JDK 8 source compatibility.
  86  *
  87  * It is used internally in the JDK to implement jimage/jrtfs access,
  88  * but also compiled and delivered as part of the jrtfs.jar to support access
  89  * to the jimage file provided by the shipped JDK by tools running on JDK 8.
  90  */
  91 public final class ImageReader implements AutoCloseable {
  92 
  93     // For resource paths, there's no leading '/'.
  94     private static final String PREVIEW_RESOURCE_PREFIX = PREVIEW_INFIX.substring(1);
  95 
  96     private final SharedImageReader reader;
  97 
  98     private volatile boolean closed;
  99 
 100     private ImageReader(SharedImageReader reader) {
 101         this.reader = reader;
 102     }
 103 
 104     /**
 105      * Opens an image reader for a jimage file at the specified path.
 106      *
 107      * @param imagePath file system path of the jimage file.
 108      * @param mode whether to return preview resources.
 109      */
 110     public static ImageReader open(Path imagePath, PreviewMode mode) throws IOException {
 111         return open(imagePath, ByteOrder.nativeOrder(), mode);
 112     }
 113 
 114     /**
 115      * Opens an image reader for a jimage file at the specified path.
 116      *
 117      * @param imagePath file system path of the jimage file.
 118      * @param byteOrder the byte-order to be used when reading the jimage file.
 119      * @param mode controls whether preview resources are visible.
 120      */
 121     public static ImageReader open(Path imagePath, ByteOrder byteOrder, PreviewMode mode)
 122             throws IOException {
 123         Objects.requireNonNull(imagePath);
 124         Objects.requireNonNull(byteOrder);
 125         return SharedImageReader.open(imagePath, byteOrder, mode.isPreviewModeEnabled());
 126     }
 127 
 128     @Override
 129     public void close() throws IOException {
 130         if (closed) {
 131             throw new IOException("image file already closed");
 132         }
 133         reader.close(this);
 134         closed = true;
 135     }
 136 
 137     private void ensureOpen() throws IOException {
 138         if (closed) {
 139             throw new IOException("image file closed");
 140         }
 141     }
 142 
 143     private void requireOpen() {
 144         if (closed) {
 145             throw new IllegalStateException("image file closed");
 146         }
 147     }
 148 
 149     /**
 150      * Finds the node with the given name.
 151      *
 152      * @param name a node name of the form {@code "/modules/<module>/...} or
 153      *     {@code "/packages/<package>/...}.
 154      * @return a node representing a resource, directory or symbolic link.
 155      */
 156     public Node findNode(String name) throws IOException {
 157         ensureOpen();
 158         return reader.findNode(name);
 159     }
 160 
 161     /**
 162      * Returns a resource node in the given module, or null if no resource of
 163      * that name exists.
 164      *
 165      * <p>This is equivalent to:
 166      * <pre>{@code
 167      * findNode("/modules/" + moduleName + "/" + resourcePath)
 168      * }</pre>
 169      * but more performant, and returns {@code null} for directories.
 170      *
 171      * @param moduleName The module name of the requested resource.
 172      * @param resourcePath Trailing module-relative resource path, not starting
 173      *     with {@code '/'}.
 174      */
 175     public Node findResourceNode(String moduleName, String resourcePath)
 176             throws IOException {
 177         ensureOpen();
 178         return reader.findResourceNode(moduleName, resourcePath);
 179     }
 180 
 181     /**
 182      * Returns whether a resource exists in the given module.
 183      *
 184      * <p>This is equivalent to:
 185      * <pre>{@code
 186      * findResourceNode(moduleName, resourcePath) != null
 187      * }</pre>
 188      * but more performant, and will not create or cache new nodes.
 189      *
 190      * @param moduleName The module name of the resource being tested for.
 191      * @param resourcePath Trailing module-relative resource path, not starting
 192      *     with {@code '/'}.
 193      */
 194     public boolean containsResource(String moduleName, String resourcePath)
 195             throws IOException {
 196         ensureOpen();
 197         return reader.containsResource(moduleName, resourcePath);
 198     }
 199 
 200     /**
 201      * Returns a copy of the content of a resource node. The buffer returned by
 202      * this method is not cached by the node, and each call returns a new array
 203      * instance.
 204      *
 205      * @throws IOException if the content cannot be returned (including if the
 206      * given node is not a resource node).
 207      */
 208     public byte[] getResource(Node node) throws IOException {
 209         ensureOpen();
 210         return reader.getResource(node);
 211     }
 212 
 213     /**
 214      * Returns the content of a resource node in a newly allocated byte buffer.
 215      */
 216     public ByteBuffer getResourceBuffer(Node node) {
 217         requireOpen();
 218         if (!node.isResource()) {
 219             throw new IllegalArgumentException("Not a resource node: " + node);
 220         }
 221         return reader.getResourceBuffer(node.getLocation());
 222     }
 223 
 224     // Package protected for use only by SystemImageReader.
 225     ResourceEntries getResourceEntries() {
 226         return reader.getResourceEntries();
 227     }
 228 
 229     private static final class SharedImageReader extends BasicImageReader {
 230         // There are >30,000 nodes in a complete jimage tree, and even relatively
 231         // common tasks (e.g. starting up javac) load somewhere in the region of
 232         // 1000 classes. Thus, an initial capacity of 2000 is a reasonable guess.
 233         private static final int INITIAL_NODE_CACHE_CAPACITY = 2000;
 234 
 235         static final class ReaderKey {
 236             private final Path imagePath;
 237             private final boolean previewMode;
 238 
 239             public ReaderKey(Path imagePath, boolean previewMode) {
 240                 this.imagePath = imagePath;
 241                 this.previewMode = previewMode;
 242             }
 243 
 244             @Override
 245             public boolean equals(Object obj) {
 246                 // No pattern variables here (Java 8 compatible source).
 247                 if (obj instanceof ReaderKey) {
 248                     ReaderKey other = (ReaderKey) obj;
 249                     return this.imagePath.equals(other.imagePath) && this.previewMode == other.previewMode;
 250                 }
 251                 return false;
 252             }
 253 
 254             @Override
 255             public int hashCode() {
 256                 return imagePath.hashCode() ^ Boolean.hashCode(previewMode);
 257             }
 258         }
 259 
 260         private static final Map<ReaderKey, SharedImageReader> OPEN_FILES = new HashMap<>();
 261 
 262         // List of openers for this shared image.
 263         private final Set<ImageReader> openers = new HashSet<>();
 264 
 265         // Attributes of the jimage file. The jimage file does not contain
 266         // attributes for the individual resources (yet). We use attributes
 267         // of the jimage file itself (creation, modification, access times).
 268         private final BasicFileAttributes imageFileAttributes;
 269 
 270         // Cache of all user visible nodes, guarded by synchronizing 'this' instance.
 271         private final Map<String, Node> nodes;
 272 
 273         // Preview mode support.
 274         private final boolean previewMode;
 275         // A relativized mapping from non-preview name to directories containing
 276         // preview-only nodes. This is used to add preview-only content to
 277         // directories as they are completed.
 278         private final HashMap<String, Directory> previewDirectoriesToMerge;
 279 
 280         private SharedImageReader(Path imagePath, ByteOrder byteOrder, boolean previewMode) throws IOException {
 281             super(imagePath, byteOrder);
 282             this.imageFileAttributes = Files.readAttributes(imagePath, BasicFileAttributes.class);
 283             this.nodes = new HashMap<>(INITIAL_NODE_CACHE_CAPACITY);
 284             this.previewMode = previewMode;
 285 
 286             // Node creation is very lazy, so we can just make the top-level directories
 287             // now without the risk of triggering the building of lots of other nodes.
 288             Directory packages = ensureCached(newDirectory(PACKAGES_PREFIX));
 289             Directory modules = ensureCached(newDirectory(MODULES_PREFIX));
 290 
 291             Directory root = newDirectory("/");
 292             root.setChildren(Arrays.asList(packages, modules));
 293             ensureCached(root);
 294 
 295             // By scanning the /packages directory information early we can determine
 296             // which module/package pairs have preview resources, and build the (small)
 297             // set of preview nodes early. This also ensures that preview-only entries
 298             // in the /packages directory are not present in non-preview mode.
 299             this.previewDirectoriesToMerge = previewMode ? new HashMap<>() : null;
 300             packages.setChildren(processPackagesDirectory(previewMode));
 301         }
 302 
 303         /**
 304          * Process {@code "/packages/xxx"} entries to build the child nodes for the
 305          * root {@code "/packages"} node. Preview-only entries will be skipped if
 306          * {@code previewMode == false}.
 307          *
 308          * <p>If {@code previewMode == true}, this method also populates the {@link
 309          * #previewDirectoriesToMerge} map with any preview-only nodes, to be merged
 310          * into directories as they are completed. It also caches preview resources
 311          * and preview-only directories for direct lookup.
 312          */
 313         private ArrayList<Node> processPackagesDirectory(boolean previewMode) {
 314             ImageLocation pkgRoot = findLocation(PACKAGES_PREFIX);
 315             assert pkgRoot != null : "Invalid jimage file";
 316             IntBuffer offsets = getOffsetBuffer(pkgRoot);
 317             ArrayList<Node> pkgDirs = new ArrayList<>(offsets.capacity());
 318             // Package path to module map, sorted in reverse order so that
 319             // longer child paths get processed first.
 320             Map<String, List<String>> previewPackagesToModules =
 321                     new TreeMap<>(Comparator.reverseOrder());
 322             for (int i = 0; i < offsets.capacity(); i++) {
 323                 ImageLocation pkgDir = getLocation(offsets.get(i));
 324                 int flags = pkgDir.getFlags();
 325                 // A package subdirectory is "preview only" if all the modules
 326                 // it references have that package marked as preview only.
 327                 // Skipping these entries avoids empty package subdirectories.
 328                 if (previewMode || !ImageLocation.isPreviewOnly(flags)) {
 329                     pkgDirs.add(ensureCached(newDirectory(pkgDir.getFullName())));
 330                 }
 331                 if (previewMode && ImageLocation.hasPreviewVersion(flags)) {
 332                     // Only do this in preview mode for the small set of packages with
 333                     // preview versions (the number of preview entries should be small).
 334                     List<String> moduleNames = new ArrayList<>();
 335                     ModuleReference.readNameOffsets(getOffsetBuffer(pkgDir), /*normal*/ false, /*preview*/ true)
 336                             .forEachRemaining(n -> moduleNames.add(getString(n)));
 337                     previewPackagesToModules.put(pkgDir.getBase().replace('.', '/'), moduleNames);
 338                 }
 339             }
 340             // Reverse sorted map means child directories are processed first.
 341             previewPackagesToModules.forEach((pkgPath, modules) ->
 342                     modules.forEach(modName -> processPreviewDir(MODULES_PREFIX + "/" + modName, pkgPath)));
 343             // We might have skipped some preview-only package entries.
 344             pkgDirs.trimToSize();
 345             return pkgDirs;
 346         }
 347 
 348         void processPreviewDir(String namePrefix, String pkgPath) {
 349             String previewDirName = namePrefix + PREVIEW_INFIX + "/" + pkgPath;
 350             ImageLocation previewLoc = findLocation(previewDirName);
 351             assert previewLoc != null : "Missing preview directory location: " + previewDirName;
 352             String nonPreviewDirName = namePrefix + "/" + pkgPath;
 353             List<Node> previewOnlyChildren = createChildNodes(previewLoc, 0, childLoc -> {
 354                 String baseName = getBaseName(childLoc);
 355                 String nonPreviewChildName = nonPreviewDirName + "/" + baseName;
 356                 boolean isPreviewOnly = ImageLocation.isPreviewOnly(childLoc.getFlags());
 357                 LocationType type = childLoc.getType();
 358                 if (type == RESOURCE) {
 359                     // Preview resources are cached to override non-preview versions.
 360                     Node childNode = ensureCached(newResource(nonPreviewChildName, childLoc));
 361                     return isPreviewOnly ? childNode : null;
 362                 } else {
 363                     // Child directories are not cached here (they are either cached
 364                     // already or have been added to previewDirectoriesToMerge).
 365                     assert type == MODULES_DIR : "Invalid location type: " + childLoc;
 366                     Node childNode = nodes.get(nonPreviewChildName);
 367                     assert isPreviewOnly == (childNode != null) :
 368                             "Inconsistent child node: " + nonPreviewChildName;
 369                     return childNode;
 370                 }
 371             });
 372             Directory previewDir = newDirectory(nonPreviewDirName);
 373             previewDir.setChildren(previewOnlyChildren);
 374             if (ImageLocation.isPreviewOnly(previewLoc.getFlags())) {
 375                 // If we are preview-only, our children are also preview-only, so
 376                 // this directory is a complete hierarchy and should be cached.
 377                 assert !previewOnlyChildren.isEmpty() : "Invalid empty preview-only directory: " + nonPreviewDirName;
 378                 ensureCached(previewDir);
 379             } else if (!previewOnlyChildren.isEmpty()) {
 380                 // A partial directory containing extra preview-only nodes
 381                 // to be merged when the non-preview directory is completed.
 382                 previewDirectoriesToMerge.put(nonPreviewDirName, previewDir);
 383             }
 384         }
 385 
 386         // Adds a node to the cache, ensuring that no matching entry already existed.
 387         private <T extends Node> T ensureCached(T node) {
 388             Node existingNode = nodes.put(node.getName(), node);
 389             assert existingNode == null : "Unexpected node already cached for: " + node;
 390             return node;
 391         }
 392 
 393         private static ImageReader open(Path imagePath, ByteOrder byteOrder, boolean previewMode) throws IOException {
 394             Objects.requireNonNull(imagePath);
 395             Objects.requireNonNull(byteOrder);
 396 
 397             synchronized (OPEN_FILES) {
 398                 ReaderKey key = new ReaderKey(imagePath, previewMode);
 399                 SharedImageReader reader = OPEN_FILES.get(key);
 400 
 401                 if (reader == null) {
 402                     // Will fail with an IOException if wrong byteOrder.
 403                     reader = new SharedImageReader(imagePath, byteOrder, previewMode);
 404                     OPEN_FILES.put(key, reader);
 405                 } else if (reader.getByteOrder() != byteOrder) {
 406                     throw new IOException("\"" + reader.getName() + "\" is not an image file");
 407                 }
 408 
 409                 ImageReader image = new ImageReader(reader);
 410                 reader.openers.add(image);
 411 
 412                 return image;
 413             }
 414         }
 415 
 416         public void close(ImageReader image) throws IOException {
 417             Objects.requireNonNull(image);
 418 
 419             synchronized (OPEN_FILES) {
 420                 if (!openers.remove(image)) {
 421                     throw new IOException("image file already closed");
 422                 }
 423 
 424                 if (openers.isEmpty()) {
 425                     close();
 426                     nodes.clear();
 427 
 428                     if (!OPEN_FILES.remove(new ReaderKey(getImagePath(), previewMode), this)) {
 429                         throw new IOException("image file not found in open list");
 430                     }
 431                 }
 432             }
 433         }
 434 
 435         /**
 436          * Returns a node with the given name, or null if no resource or directory of
 437          * that name exists.
 438          *
 439          * <p>Note that there is no reentrant calling back to this method from within
 440          * the node handling code.
 441          *
 442          * @param name an absolute, {@code /}-separated path string, prefixed with either
 443          *     "/modules" or "/packages".
 444          */
 445         synchronized Node findNode(String name) {
 446             // Root directories "/", "/modules" and "/packages", as well
 447             // as all "/packages/xxx" subdirectories are already cached.
 448             Node node = nodes.get(name);
 449             if (node == null) {
 450                 if (name.startsWith(MODULES_PREFIX + "/")) {
 451                     node = buildAndCacheModulesNode(name);
 452                 } else if (name.startsWith(PACKAGES_PREFIX + "/")) {
 453                     node = buildAndCacheLinkNode(name);
 454                 }
 455             } else if (!node.isCompleted()) {
 456                 // Only directories can be incomplete.
 457                 assert node instanceof Directory : "Invalid incomplete node: " + node;
 458                 completeDirectory((Directory) node);
 459             }
 460             assert node == null || node.isCompleted() : "Incomplete node: " + node;
 461             return node;
 462         }
 463 
 464         /**
 465          * Returns a resource node in the given module, or null if no resource of
 466          * that name exists.
 467          *
 468          * <p>Note that there is no reentrant calling back to this method from within
 469          * the node handling code.
 470          */
 471         Node findResourceNode(String moduleName, String resourcePath) {
 472             // Unlike findNode(), this method makes only one lookup in the
 473             // underlying jimage, but can only reliably return resource nodes.
 474             if (moduleName.indexOf('/') >= 0) {
 475                 throw new IllegalArgumentException("invalid module name: " + moduleName);
 476             }
 477             if (resourcePath.startsWith(PREVIEW_RESOURCE_PREFIX)) {
 478                 return null;
 479             }
 480             String nodeName = MODULES_PREFIX + "/" + moduleName + "/" + resourcePath;
 481             // Synchronize as tightly as possible to reduce locking contention.
 482             synchronized (this) {
 483                 Node node = nodes.get(nodeName);
 484                 if (node == null) {
 485                     ImageLocation loc = null;
 486                     if (previewMode) {
 487                         // We must test preview location first (if in preview mode).
 488                         loc = findLocation(moduleName, PREVIEW_RESOURCE_PREFIX + resourcePath);
 489                     }
 490                     if (loc == null) {
 491                         loc = findLocation(moduleName, resourcePath);
 492                     }
 493                     if (loc != null && loc.getType() == RESOURCE) {
 494                         node = newResource(nodeName, loc);
 495                         nodes.put(node.getName(), node);
 496                     }
 497                     return node;
 498                 } else {
 499                     return node.isResource() ? node : null;
 500                 }
 501             }
 502         }
 503 
 504         /**
 505          * Returns whether a resource exists in the given module.
 506          *
 507          * <p>This method is expected to be called frequently for resources
 508          * which do not exist in the given module (e.g. as part of classpath
 509          * search). As such, it skips checking the nodes cache if possible, and
 510          * only checks for an entry in the jimage file, as this is faster if the
 511          * resource is not present. This also means it doesn't need
 512          * synchronization most of the time.
 513          */
 514         boolean containsResource(String moduleName, String resourcePath) {
 515             if (moduleName.indexOf('/') >= 0) {
 516                 throw new IllegalArgumentException("invalid module name: " + moduleName);
 517             }
 518             if (resourcePath.startsWith(PREVIEW_RESOURCE_PREFIX)) {
 519                 return false;
 520             }
 521             // In preview mode, preview-only resources are eagerly added to the
 522             // cache, so we must check that first.
 523             ImageLocation loc = null;
 524             if (previewMode) {
 525                 String nodeName = MODULES_PREFIX + "/" + moduleName + "/" + resourcePath;
 526                 // Synchronize as tightly as possible to reduce locking contention.
 527                 synchronized (this) {
 528                     Node node = nodes.get(nodeName);
 529                     if (node != null) {
 530                         return node.isResource();
 531                     }
 532                 }
 533                 loc = findLocation(moduleName, PREVIEW_RESOURCE_PREFIX + resourcePath);
 534             }
 535             if (loc == null) {
 536                 loc = findLocation(moduleName, resourcePath);
 537             }
 538             return loc != null && loc.getType() == RESOURCE;
 539         }
 540 
 541         /**
 542          * Builds a node in the "/modules/..." namespace.
 543          *
 544          * <p>Called by {@link #findNode(String)} if a {@code /modules/...} node
 545          * is not present in the cache.
 546          */
 547         private Node buildAndCacheModulesNode(String name) {
 548             assert name.startsWith(MODULES_PREFIX + "/") : "Invalid module node name: " + name;
 549             if (isPreviewName(name)) {
 550                 return null;
 551             }
 552             // Returns null for non-directory resources, since the jimage name does not
 553             // start with "/modules" (e.g. "/java.base/java/lang/Object.class").
 554             ImageLocation loc = findLocation(name);
 555             if (loc != null) {
 556                 assert name.equals(loc.getFullName()) : "Mismatched location for directory: " + name;
 557                 assert loc.getType() == MODULES_DIR : "Invalid modules directory: " + name;
 558                 return ensureCached(completeModuleDirectory(newDirectory(name), loc));
 559             }
 560             // Now try the non-prefixed resource name, but be careful to avoid false
 561             // positives for names like "/modules/modules/xxx" which could return a
 562             // location of a directory entry.
 563             loc = findLocation(name.substring(MODULES_PREFIX.length()));
 564             return loc != null && loc.getType() == RESOURCE
 565                     ? ensureCached(newResource(name, loc))
 566                     : null;
 567         }
 568 
 569         /**
 570          * Returns whether a directory name in the "/modules/" directory could be referencing
 571          * the "META-INF" directory".
 572          */
 573         private boolean isMetaInf(Directory dir) {
 574             String name = dir.getName();
 575             int pathStart = name.indexOf('/', MODULES_PREFIX.length() + 1);
 576             return name.length() == pathStart + "/META-INF".length()
 577                     && name.endsWith("/META-INF");
 578         }
 579 
 580         /**
 581          * Returns whether a node name in the "/modules/" directory could be referencing
 582          * a preview resource or directory under "META-INF/preview".
 583          */
 584         private boolean isPreviewName(String name) {
 585             int pathStart = name.indexOf('/', MODULES_PREFIX.length() + 1);
 586             int previewEnd = pathStart + PREVIEW_INFIX.length();
 587             return pathStart > 0
 588                     && name.regionMatches(pathStart, PREVIEW_INFIX, 0, PREVIEW_INFIX.length())
 589                     && (name.length() == previewEnd || name.charAt(previewEnd) == '/');
 590         }
 591 
 592         private String getBaseName(ImageLocation loc) {
 593             // Matches logic in ImageLocation#getFullName() regarding extensions.
 594             String trailingParts = loc.getBase()
 595                     + ((loc.getExtensionOffset() != 0) ? "." + loc.getExtension() : "");
 596             return trailingParts.substring(trailingParts.lastIndexOf('/') + 1);
 597         }
 598 
 599         /**
 600          * Builds a link node of the form "/packages/xxx/yyy".
 601          *
 602          * <p>Called by {@link #findNode(String)} if a {@code /packages/...}
 603          * node is not present in the cache (the name is not trusted).
 604          */
 605         private Node buildAndCacheLinkNode(String name) {
 606             // There are only locations for "/packages" or "/packages/xxx"
 607             // directories, but not the symbolic links below them (links are
 608             // derived from the name information in the parent directory).
 609             int packageStart = PACKAGES_PREFIX.length() + 1;
 610             int packageEnd = name.indexOf('/', packageStart);
 611             // We already built the 2-level "/packages/xxx" directories,
 612             // so if this is a 2-level name, it cannot reference a node.
 613             if (packageEnd >= 0) {
 614                 String dirName = name.substring(0, packageEnd);
 615                 // If no parent exists here, the name cannot be valid.
 616                 Directory parent = (Directory) nodes.get(dirName);
 617                 if (parent != null) {
 618                     if (!parent.isCompleted()) {
 619                         // This caches all child links of the parent directory.
 620                         completePackageSubdirectory(parent, findLocation(dirName));
 621                     }
 622                     return nodes.get(name);
 623                 }
 624             }
 625             return null;
 626         }
 627 
 628         /** Completes a directory by ensuring its child list is populated correctly. */
 629         private void completeDirectory(Directory dir) {
 630             String name = dir.getName();
 631             // Since the node exists, we can assert that its name starts with
 632             // either "/modules" or "/packages", making differentiation easy.
 633             // It also means that the name is valid, so it must yield a location.
 634             assert name.startsWith(MODULES_PREFIX) || name.startsWith(PACKAGES_PREFIX);
 635             ImageLocation loc = findLocation(name);
 636             assert loc != null && name.equals(loc.getFullName()) : "Invalid location for name: " + name;
 637             LocationType type = loc.getType();
 638             if (type == MODULES_DIR || type == MODULES_ROOT) {
 639                 completeModuleDirectory(dir, loc);
 640             } else {
 641                 assert type == PACKAGES_DIR : "Invalid location type: " + loc;
 642                 completePackageSubdirectory(dir, loc);
 643             }
 644             assert dir.isCompleted() : "Directory must be complete by now: " + dir;
 645         }
 646 
 647         /** Completes a modules directory by setting the list of child nodes. */
 648         private Directory completeModuleDirectory(Directory dir, ImageLocation loc) {
 649             assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir;
 650             List<Node> previewOnlyNodes = getPreviewNodesToMerge(dir);
 651             // We hide preview names from direct lookup, but must also prevent
 652             // the preview directory from appearing in any META-INF directories.
 653             boolean parentIsMetaInfDir = isMetaInf(dir);
 654             List<Node> children = createChildNodes(loc, previewOnlyNodes.size(), childLoc -> {
 655                 LocationType type = childLoc.getType();
 656                 if (type == MODULES_DIR) {
 657                     String name = childLoc.getFullName();
 658                     return parentIsMetaInfDir && name.endsWith("/preview")
 659                             ? null
 660                             : nodes.computeIfAbsent(name, this::newDirectory);
 661                 } else {
 662                     assert type == RESOURCE : "Invalid location type: " + loc;
 663                     // Add "/modules" prefix to image location paths to get node names.
 664                     String resourceName = childLoc.getFullName(true);
 665                     return nodes.computeIfAbsent(resourceName, n -> newResource(n, childLoc));
 666                 }
 667             });
 668             children.addAll(previewOnlyNodes);
 669             dir.setChildren(children);
 670             return dir;
 671         }
 672 
 673         /** Completes a package directory by setting the list of child nodes. */
 674         private void completePackageSubdirectory(Directory dir, ImageLocation loc) {
 675             assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir;
 676             assert !dir.isCompleted() : "Directory already completed: " + dir;
 677             assert loc.getType() == PACKAGES_DIR : "Invalid location type: " + loc.getType();
 678 
 679             // In non-preview mode we might skip a very small number of preview-only
 680             // entries, but it's not worth "right-sizing" the array for that.
 681             IntBuffer offsets = getOffsetBuffer(loc);
 682             List<Node> children = new ArrayList<>(offsets.capacity() / 2);
 683             ModuleReference.readNameOffsets(offsets, /*normal*/ true, previewMode)
 684                     .forEachRemaining(n -> {
 685                         String modName = getString(n);
 686                         Node link = newLinkNode(dir.getName() + "/" + modName, MODULES_PREFIX + "/" + modName);
 687                         children.add(ensureCached(link));
 688                     });
 689             // If the parent directory exists, there must be at least one child node.
 690             assert !children.isEmpty() : "Invalid empty package directory: " + dir;
 691             dir.setChildren(children);
 692         }
 693 
 694         /**
 695          * Returns the list of child preview nodes to be merged into the given directory.
 696          *
 697          * <p>Because this is only called once per-directory (since the result is cached
 698          * indefinitely) we can remove any entries we find from the cache. If ever the
 699          * node cache allowed entries to expire, this would have to be changed so that
 700          * directories could be completed more than once.
 701          */
 702         List<Node> getPreviewNodesToMerge(Directory dir) {
 703             if (previewDirectoriesToMerge != null) {
 704                 Directory mergeDir = previewDirectoriesToMerge.remove(dir.getName());
 705                 if (mergeDir != null) {
 706                     return mergeDir.children;
 707                 }
 708             }
 709             return Collections.emptyList();
 710         }
 711 
 712         /**
 713          * Creates the list of child nodes for a modules {@code Directory} from
 714          * its parent location.
 715          *
 716          * <p>The {@code getChildFn} may return existing cached nodes rather
 717          * than creating them, and if newly created nodes are to be cached,
 718          * it is the job of {@code getChildFn}, or the caller of this method,
 719          * to do that.
 720          *
 721          * @param loc a location relating to a "/modules" directory.
 722          * @param extraNodesCount a known number of preview-only child nodes
 723          *     which will be merged onto the end of the returned list later.
 724          * @param getChildFn a function to return a node for each child location
 725          *     (or null to skip putting anything in the list).
 726          * @return the list of the non-null child nodes, returned by
 727          *     {@code getChildFn}, in the order of the locations entries.
 728          */
 729         private List<Node> createChildNodes(ImageLocation loc, int extraNodesCount, Function<ImageLocation, Node> getChildFn) {
 730             LocationType type = loc.getType();
 731             assert type == MODULES_DIR || type == MODULES_ROOT : "Invalid location type: " + loc;
 732             IntBuffer offsets = getOffsetBuffer(loc);
 733             int childCount = offsets.capacity();
 734             List<Node> children = new ArrayList<>(childCount + extraNodesCount);
 735             for (int i = 0; i < childCount; i++) {
 736                 Node childNode = getChildFn.apply(getLocation(offsets.get(i)));
 737                 if (childNode != null) {
 738                     children.add(childNode);
 739                 }
 740             }
 741             return children;
 742         }
 743 
 744         /** Helper to extract the integer offset buffer from a directory location. */
 745         private IntBuffer getOffsetBuffer(ImageLocation dir) {
 746             assert dir.getType() != RESOURCE : "Not a directory: " + dir.getFullName();
 747             byte[] offsets = getResource(dir);
 748             ByteBuffer buffer = ByteBuffer.wrap(offsets);
 749             buffer.order(getByteOrder());
 750             return buffer.asIntBuffer();
 751         }
 752 
 753         /**
 754          * Creates an "incomplete" directory node with no child nodes set.
 755          * Directories need to be "completed" before they are returned by
 756          * {@link #findNode(String)}.
 757          */
 758         private Directory newDirectory(String name) {
 759             return new Directory(name, imageFileAttributes);
 760         }
 761 
 762         /**
 763          * Creates a new resource from an image location. This is the only case
 764          * where the image location name does not match the requested node name.
 765          * In image files, resource locations are NOT prefixed by {@code /modules}.
 766          */
 767         private Resource newResource(String name, ImageLocation loc) {
 768             return new Resource(name, loc, imageFileAttributes);
 769         }
 770 
 771         /**
 772          * Creates a new link node pointing at the given target name.
 773          *
 774          * <p>Note that target node is resolved each time {@code resolve()} is called,
 775          * so if a link node is retained after its reader is closed, it will fail.
 776          */
 777         private LinkNode newLinkNode(String name, String targetName) {
 778             return new LinkNode(name, () -> findNode(targetName), imageFileAttributes);
 779         }
 780 
 781         /** Returns the content of a resource node. */
 782         private byte[] getResource(Node node) throws IOException {
 783             // We could have been given a non-resource node here.
 784             if (node.isResource()) {
 785                 return super.getResource(node.getLocation());
 786             }
 787             throw new IOException("Not a resource: " + node);
 788         }
 789     }
 790 
 791     /**
 792      * A directory, resource or symbolic link.
 793      *
 794      * <h3 id="node_equality">Node Equality</h3>
 795      *
 796      * Nodes are identified solely by their name, and it is not valid to attempt
 797      * to compare nodes from different reader instances. Different readers may
 798      * produce nodes with the same names, but different contents.
 799      *
 800      * <p>Furthermore, since a {@link ImageReader} provides "perfect" caching of
 801      * nodes, equality of nodes from the same reader is equivalent to instance
 802      * identity.
 803      */
 804     public abstract static class Node {
 805         private final String name;
 806         private final BasicFileAttributes fileAttrs;
 807 
 808         /**
 809          * Creates an abstract {@code Node}, which is either a resource, directory
 810          * or symbolic link.
 811          *
 812          * <p>This constructor is only non-private so it can be used by the
 813          * {@code ExplodedImage} class, and must not be used otherwise.
 814          */
 815         protected Node(String name, BasicFileAttributes fileAttrs) {
 816             this.name = Objects.requireNonNull(name);
 817             this.fileAttrs = Objects.requireNonNull(fileAttrs);
 818         }
 819 
 820         // A node is completed when all its direct children have been built.
 821         // As such, non-directory nodes are always complete.
 822         boolean isCompleted() {
 823             return true;
 824         }
 825 
 826         // Only resources can return a location.
 827         ImageLocation getLocation() {
 828             throw new IllegalStateException("not a resource: " + getName());
 829         }
 830 
 831         /**
 832          * Returns the name of this node (e.g. {@code
 833          * "/modules/java.base/java/lang/Object.class"} or {@code
 834          * "/packages/java.lang"}).
 835          *
 836          * <p>Note that for resource nodes this is NOT the underlying jimage
 837          * resource name (it is prefixed with {@code "/modules"}).
 838          */
 839         public final String getName() {
 840             return name;
 841         }
 842 
 843         /**
 844          * Returns file attributes for this node. The value returned may be the
 845          * same for all nodes, and should not be relied upon for accuracy.
 846          */
 847         public final BasicFileAttributes getFileAttributes() {
 848             return fileAttrs;
 849         }
 850 
 851         /**
 852          * Resolves a symbolic link to its target node. If this code is not a
 853          * symbolic link, then it resolves to itself.
 854          */
 855         public final Node resolveLink() {
 856             return resolveLink(false);
 857         }
 858 
 859         /**
 860          * Resolves a symbolic link to its target node. If this code is not a
 861          * symbolic link, then it resolves to itself.
 862          */
 863         public Node resolveLink(boolean recursive) {
 864             return this;
 865         }
 866 
 867         /** Returns whether this node is a symbolic link. */
 868         public boolean isLink() {
 869             return false;
 870         }
 871 
 872         /**
 873          * Returns whether this node is a directory. Directory nodes can have
 874          * {@link #getChildNames()} invoked to get the fully qualified names
 875          * of any child nodes.
 876          */
 877         public boolean isDirectory() {
 878             return false;
 879         }
 880 
 881         /**
 882          * Returns whether this node is a resource. Resource nodes can have
 883          * their contents obtained via {@link ImageReader#getResource(Node)}
 884          * or {@link ImageReader#getResourceBuffer(Node)}.
 885          */
 886         public boolean isResource() {
 887             return false;
 888         }
 889 
 890         /**
 891          * Returns the fully qualified names of any child nodes for a directory.
 892          *
 893          * <p>By default, this method throws {@link IllegalStateException} and
 894          * is overridden for directories.
 895          */
 896         public Stream<String> getChildNames() {
 897             throw new IllegalStateException("not a directory: " + getName());
 898         }
 899 
 900         /**
 901          * Returns the uncompressed size of this node's content. If this node is
 902          * not a resource, this method returns zero.
 903          */
 904         public long size() {
 905             return 0L;
 906         }
 907 
 908         /**
 909          * Returns the compressed size of this node's content. If this node is
 910          * not a resource, this method returns zero.
 911          */
 912         public long compressedSize() {
 913             return 0L;
 914         }
 915 
 916         /**
 917          * Returns the extension string of a resource node. If this node is not
 918          * a resource, this method returns null.
 919          */
 920         public String extension() {
 921             return null;
 922         }
 923 
 924         @Override
 925         public final String toString() {
 926             return getName();
 927         }
 928 
 929         /** See <a href="#node_equality">Node Equality</a>. */
 930         @Override
 931         public final int hashCode() {
 932             return name.hashCode();
 933         }
 934 
 935         /** See <a href="#node_equality">Node Equality</a>. */
 936         @Override
 937         public final boolean equals(Object other) {
 938             if (this == other) {
 939                 return true;
 940             }
 941 
 942             if (other instanceof Node) {
 943                 return name.equals(((Node) other).name);
 944             }
 945 
 946             return false;
 947         }
 948     }
 949 
 950     /**
 951      * Directory node (referenced from a full path, without a trailing '/').
 952      *
 953      * <p>Directory nodes have two distinct states:
 954      * <ul>
 955      *     <li>Incomplete: The child list has not been set.
 956      *     <li>Complete: The child list has been set.
 957      * </ul>
 958      *
 959      * <p>When a directory node is returned by {@link ImageReader#findNode(String)}
 960      * it is always complete, but this DOES NOT mean that its child nodes are
 961      * complete yet.
 962      *
 963      * <p>To avoid users being able to access incomplete child nodes, the
 964      * {@code Node} API offers only a way to obtain child node names, forcing
 965      * callers to invoke {@code findNode()} if they need to access the child
 966      * node itself.
 967      *
 968      * <p>This approach allows directories to be implemented lazily with respect
 969      * to child nodes, while retaining efficiency when child nodes are accessed
 970      * (since any incomplete nodes will be created and placed in the node cache
 971      * when the parent was first returned to the user).
 972      */
 973     private static final class Directory extends Node {
 974         // Monotonic reference, will be set to the unmodifiable child list exactly once.
 975         private List<Node> children = null;
 976 
 977         private Directory(String name, BasicFileAttributes fileAttrs) {
 978             super(name, fileAttrs);
 979         }
 980 
 981         @Override
 982         boolean isCompleted() {
 983             return children != null;
 984         }
 985 
 986         @Override
 987         public boolean isDirectory() {
 988             return true;
 989         }
 990 
 991         @Override
 992         public Stream<String> getChildNames() {
 993             if (children != null) {
 994                 return children.stream().map(Node::getName);
 995             }
 996             throw new IllegalStateException("Cannot get child nodes of an incomplete directory: " + getName());
 997         }
 998 
 999         private void setChildren(List<? extends Node> children) {
1000             assert this.children == null : this + ": Cannot set child nodes twice!";
1001             this.children = Collections.unmodifiableList(children);
1002         }
1003     }
1004 
1005     /**
1006      * Resource node (e.g. a ".class" entry, or any other data resource).
1007      *
1008      * <p>Resources are leaf nodes referencing an underlying image location. They
1009      * are lightweight, and do not cache their contents.
1010      *
1011      * <p>Unlike directories (where the node name matches the jimage path for the
1012      * corresponding {@code ImageLocation}), resource node names are NOT the same
1013      * as the corresponding jimage path. The difference is that node names for
1014      * resources are prefixed with "/modules", which is missing from the
1015      * equivalent jimage path.
1016      */
1017     private static class Resource extends Node {
1018         private final ImageLocation loc;
1019 
1020         private Resource(String name, ImageLocation loc, BasicFileAttributes fileAttrs) {
1021             super(name, fileAttrs);
1022             this.loc = loc;
1023         }
1024 
1025         @Override
1026         ImageLocation getLocation() {
1027             return loc;
1028         }
1029 
1030         @Override
1031         public boolean isResource() {
1032             return true;
1033         }
1034 
1035         @Override
1036         public long size() {
1037             return loc.getUncompressedSize();
1038         }
1039 
1040         @Override
1041         public long compressedSize() {
1042             return loc.getCompressedSize();
1043         }
1044 
1045         @Override
1046         public String extension() {
1047             return loc.getExtension();
1048         }
1049     }
1050 
1051     /**
1052      * Link node (a symbolic link to a top-level modules directory).
1053      *
1054      * <p>Link nodes resolve their target by invoking a given supplier, and do
1055      * not cache the result. Since nodes are cached by the {@code ImageReader},
1056      * this means that only the first call to {@link #resolveLink(boolean)}
1057      * could do any significant work.
1058      */
1059     private static class LinkNode extends Node {
1060         private final Supplier<Node> link;
1061 
1062         private LinkNode(String name, Supplier<Node> link, BasicFileAttributes fileAttrs) {
1063             super(name, fileAttrs);
1064             this.link = link;
1065         }
1066 
1067         @Override
1068         public Node resolveLink(boolean recursive) {
1069             // No need to use or propagate the recursive flag, since the target
1070             // cannot possibly be a link node (links only point to directories).
1071             return link.get();
1072         }
1073 
1074         @Override
1075         public boolean isLink() {
1076             return true;
1077         }
1078     }
1079 }