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