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