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 java.io.IOException;
 28 import java.nio.ByteBuffer;
 29 import java.nio.ByteOrder;
 30 import java.nio.IntBuffer;
 31 import java.nio.file.Files;
 32 import java.nio.file.Path;
 33 import java.nio.file.attribute.BasicFileAttributes;
 34 import java.util.ArrayList;
 35 import java.util.Arrays;
 36 import java.util.Collections;
 37 import java.util.HashMap;
 38 import java.util.HashSet;
 39 import java.util.List;
 40 import java.util.Map;
 41 import java.util.Objects;
 42 import java.util.Set;
 43 import java.util.function.Function;
 44 import java.util.function.Supplier;
 45 import java.util.stream.Stream;
 46 
 47 /**
 48  * A view over the entries of a jimage file with a unified namespace suitable
 49  * for file system use. The jimage entries (resources, module and package
 50  * information) are mapped into a unified hierarchy of named nodes, which serve
 51  * as the underlying structure for {@code JrtFileSystem} and other utilities.
 52  *
 53  * <p>Entries in jimage are expressed as one of three {@link Node} types;
 54  * resource nodes, directory nodes and link nodes.
 55  *
 56  * <p>When remapping jimage entries, jimage location names (e.g. {@code
 57  * "/java.base/java/lang/Integer.class"}) are prefixed with {@code "/modules"}
 58  * to form the names of resource nodes. This aligns with the naming of module
 59  * entries in jimage (e.g. "/modules/java.base/java/lang"), which appear as
 60  * directory nodes in {@code ImageReader}.
 61  *
 62  * <p>Package entries (e.g. {@code "/packages/java.lang"} appear as directory
 63  * nodes containing link nodes, which resolve back to the root directory of the
 64  * module in which that package exists (e.g. {@code "/modules/java.base"}).
 65  * Unlike other nodes, the jimage file does not contain explicit entries for
 66  * link nodes, and their existence is derived only from the contents of the
 67  * parent directory.
 68  *
 69  * <p>While similar to {@code BasicImageReader}, this class is not a conceptual
 70  * subtype of it, and deliberately hides types such as {@code ImageLocation} to
 71  * give a focused API based only on nodes.
 72  *
 73  * @implNote This class needs to maintain JDK 8 source compatibility.
 74  *
 75  * It is used internally in the JDK to implement jimage/jrtfs access,
 76  * but also compiled and delivered as part of the jrtfs.jar to support access
 77  * to the jimage file provided by the shipped JDK by tools running on JDK 8.
 78  */
 79 public final class ImageReader implements AutoCloseable {
 80     private final SharedImageReader reader;
 81 
 82     private volatile boolean closed;
 83 
 84     private ImageReader(SharedImageReader reader) {
 85         this.reader = reader;
 86     }
 87 
 88     /**
 89      * Opens an image reader for a jimage file at the specified path, using the
 90      * given byte order.
 91      */
 92     public static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException {
 93         Objects.requireNonNull(imagePath);
 94         Objects.requireNonNull(byteOrder);
 95 
 96         return SharedImageReader.open(imagePath, byteOrder);
 97     }
 98 
 99     /**
100      * Opens an image reader for a jimage file at the specified path, using the
101      * platform native byte order.
102      */
103     public static ImageReader open(Path imagePath) throws IOException {
104         return open(imagePath, ByteOrder.nativeOrder());
105     }
106 
107     @Override
108     public void close() throws IOException {
109         if (closed) {
110             throw new IOException("image file already closed");
111         }
112         reader.close(this);
113         closed = true;
114     }
115 
116     private void ensureOpen() throws IOException {
117         if (closed) {
118             throw new IOException("image file closed");
119         }
120     }
121 
122     private void requireOpen() {
123         if (closed) {
124             throw new IllegalStateException("image file closed");
125         }
126     }
127 
128     /**
129      * Finds the node with the given name.
130      *
131      * @param name a node name of the form {@code "/modules/<module>/...} or
132      *     {@code "/packages/<package>/...}.
133      * @return a node representing a resource, directory or symbolic link.
134      */
135     public Node findNode(String name) throws IOException {
136         ensureOpen();
137         return reader.findNode(name);
138     }
139 
140     /**
141      * Returns a resource node in the given module, or null if no resource of
142      * that name exists.
143      *
144      * <p>This is equivalent to:
145      * <pre>{@code
146      * findNode("/modules/" + moduleName + "/" + resourcePath)
147      * }</pre>
148      * but more performant, and returns {@code null} for directories.
149      *
150      * @param moduleName The module name of the requested resource.
151      * @param resourcePath Trailing module-relative resource path, not starting
152      *     with {@code '/'}.
153      */
154     public Node findResourceNode(String moduleName, String resourcePath)
155             throws IOException {
156         ensureOpen();
157         return reader.findResourceNode(moduleName, resourcePath);
158     }
159 
160     /**
161      * Returns whether a resource exists in the given module.
162      *
163      * <p>This is equivalent to:
164      * <pre>{@code
165      * findResourceNode(moduleName, resourcePath) != null
166      * }</pre>
167      * but more performant, and will not create or cache new nodes.
168      *
169      * @param moduleName The module name of the resource being tested for.
170      * @param resourcePath Trailing module-relative resource path, not starting
171      *     with {@code '/'}.
172      */
173     public boolean containsResource(String moduleName, String resourcePath)
174             throws IOException {
175         ensureOpen();
176         return reader.containsResource(moduleName, resourcePath);
177     }
178 
179     /**
180      * Returns a copy of the content of a resource node. The buffer returned by
181      * this method is not cached by the node, and each call returns a new array
182      * instance.
183      *
184      * @throws IOException if the content cannot be returned (including if the
185      * given node is not a resource node).
186      */
187     public byte[] getResource(Node node) throws IOException {
188         ensureOpen();
189         return reader.getResource(node);
190     }
191 
192     /**
193      * Releases a (possibly cached) {@link ByteBuffer} obtained via
194      * {@link #getResourceBuffer(Node)}.
195      *
196      * <p>Note that no testing is performed to check whether the buffer about
197      * to be released actually came from a call to {@code getResourceBuffer()}.
198      */
199     public static void releaseByteBuffer(ByteBuffer buffer) {
200         BasicImageReader.releaseByteBuffer(buffer);
201     }
202 
203     /**
204      * Returns the content of a resource node in a possibly cached byte buffer.
205      * Callers of this method must call {@link #releaseByteBuffer(ByteBuffer)}
206      * when they are finished with it.
207      */
208     public ByteBuffer getResourceBuffer(Node node) {
209         requireOpen();
210         if (!node.isResource()) {
211             throw new IllegalArgumentException("Not a resource node: " + node);
212         }
213         return reader.getResourceBuffer(node.getLocation());
214     }
215 
216     private static final class SharedImageReader extends BasicImageReader {
217         private static final Map<Path, SharedImageReader> OPEN_FILES = new HashMap<>();
218         private static final String MODULES_ROOT = "/modules";
219         private static final String PACKAGES_ROOT = "/packages";
220         // There are >30,000 nodes in a complete jimage tree, and even relatively
221         // common tasks (e.g. starting up javac) load somewhere in the region of
222         // 1000 classes. Thus, an initial capacity of 2000 is a reasonable guess.
223         private static final int INITIAL_NODE_CACHE_CAPACITY = 2000;
224 
225         // List of openers for this shared image.
226         private final Set<ImageReader> openers = new HashSet<>();
227 
228         // Attributes of the jimage file. The jimage file does not contain
229         // attributes for the individual resources (yet). We use attributes
230         // of the jimage file itself (creation, modification, access times).
231         private final BasicFileAttributes imageFileAttributes;
232 
233         // Cache of all user visible nodes, guarded by synchronizing 'this' instance.
234         private final Map<String, Node> nodes;
235         // Used to classify ImageLocation instances without string comparison.
236         private final int modulesStringOffset;
237         private final int packagesStringOffset;
238 
239         private SharedImageReader(Path imagePath, ByteOrder byteOrder) throws IOException {
240             super(imagePath, byteOrder);
241             this.imageFileAttributes = Files.readAttributes(imagePath, BasicFileAttributes.class);
242             this.nodes = new HashMap<>(INITIAL_NODE_CACHE_CAPACITY);
243             // Pick stable jimage names from which to extract string offsets (we cannot
244             // use "/modules" or "/packages", since those have a module offset of zero).
245             this.modulesStringOffset = getModuleOffset("/modules/java.base");
246             this.packagesStringOffset = getModuleOffset("/packages/java.lang");
247 
248             // Node creation is very lazy, so we can just make the top-level directories
249             // now without the risk of triggering the building of lots of other nodes.
250             Directory packages = newDirectory(PACKAGES_ROOT);
251             nodes.put(packages.getName(), packages);
252             Directory modules = newDirectory(MODULES_ROOT);
253             nodes.put(modules.getName(), modules);
254 
255             Directory root = newDirectory("/");
256             root.setChildren(Arrays.asList(packages, modules));
257             nodes.put(root.getName(), root);
258         }
259 
260         /**
261          * Returns the offset of the string denoting the leading "module" segment in
262          * the given path (e.g. {@code <module>/<path>}). We can't just pass in the
263          * {@code /<module>} string here because that has a module offset of zero.
264          */
265         private int getModuleOffset(String path) {
266             ImageLocation location = findLocation(path);
267             assert location != null : "Cannot find expected jimage location: " + path;
268             int offset = location.getModuleOffset();
269             assert offset != 0 : "Invalid module offset for jimage location: " + path;
270             return offset;
271         }
272 
273         private static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException {
274             Objects.requireNonNull(imagePath);
275             Objects.requireNonNull(byteOrder);
276 
277             synchronized (OPEN_FILES) {
278                 SharedImageReader reader = OPEN_FILES.get(imagePath);
279 
280                 if (reader == null) {
281                     // Will fail with an IOException if wrong byteOrder.
282                     reader =  new SharedImageReader(imagePath, byteOrder);
283                     OPEN_FILES.put(imagePath, reader);
284                 } else if (reader.getByteOrder() != byteOrder) {
285                     throw new IOException("\"" + reader.getName() + "\" is not an image file");
286                 }
287 
288                 ImageReader image = new ImageReader(reader);
289                 reader.openers.add(image);
290 
291                 return image;
292             }
293         }
294 
295         public void close(ImageReader image) throws IOException {
296             Objects.requireNonNull(image);
297 
298             synchronized (OPEN_FILES) {
299                 if (!openers.remove(image)) {
300                     throw new IOException("image file already closed");
301                 }
302 
303                 if (openers.isEmpty()) {
304                     close();
305                     nodes.clear();
306 
307                     if (!OPEN_FILES.remove(this.getImagePath(), this)) {
308                         throw new IOException("image file not found in open list");
309                     }
310                 }
311             }
312         }
313 
314         /**
315          * Returns a node with the given name, or null if no resource or directory of
316          * that name exists.
317          *
318          * <p>Note that there is no reentrant calling back to this method from within
319          * the node handling code.
320          *
321          * @param name an absolute, {@code /}-separated path string, prefixed with either
322          *     "/modules" or "/packages".
323          */
324         synchronized Node findNode(String name) {
325             Node node = nodes.get(name);
326             if (node == null) {
327                 // We cannot get the root paths ("/modules" or "/packages") here
328                 // because those nodes are already in the nodes cache.
329                 if (name.startsWith(MODULES_ROOT + "/")) {
330                     // This may perform two lookups, one for a directory (in
331                     // "/modules/...") and one for a non-prefixed resource
332                     // (with "/modules" removed).
333                     node = buildModulesNode(name);
334                 } else if (name.startsWith(PACKAGES_ROOT + "/")) {
335                     node = buildPackagesNode(name);
336                 }
337                 if (node != null) {
338                     nodes.put(node.getName(), node);
339                 }
340             } else if (!node.isCompleted()) {
341                 // Only directories can be incomplete.
342                 assert node instanceof Directory : "Invalid incomplete node: " + node;
343                 completeDirectory((Directory) node);
344             }
345             assert node == null || node.isCompleted() : "Incomplete node: " + node;
346             return node;
347         }
348 
349         /**
350          * Returns a resource node in the given module, or null if no resource of
351          * that name exists.
352          *
353          * <p>Note that there is no reentrant calling back to this method from within
354          * the node handling code.
355          */
356         Node findResourceNode(String moduleName, String resourcePath) {
357             // Unlike findNode(), this method makes only one lookup in the
358             // underlying jimage, but can only reliably return resource nodes.
359             if (moduleName.indexOf('/') >= 0) {
360                 throw new IllegalArgumentException("invalid module name: " + moduleName);
361             }
362             String nodeName = MODULES_ROOT + "/" + moduleName + "/" + resourcePath;
363             // Synchronize as tightly as possible to reduce locking contention.
364             synchronized (this) {
365                 Node node = nodes.get(nodeName);
366                 if (node == null) {
367                     ImageLocation loc = findLocation(moduleName, resourcePath);
368                     if (loc != null && isResource(loc)) {
369                         node = newResource(nodeName, loc);
370                         nodes.put(node.getName(), node);
371                     }
372                     return node;
373                 } else {
374                     return node.isResource() ? node : null;
375                 }
376             }
377         }
378 
379         /**
380          * Returns whether a resource exists in the given module.
381          *
382          * <p>This method is expected to be called frequently for resources
383          * which do not exist in the given module (e.g. as part of classpath
384          * search). As such, it skips checking the nodes cache and only checks
385          * for an entry in the jimage file, as this is faster if the resource
386          * is not present. This also means it doesn't need synchronization.
387          */
388         boolean containsResource(String moduleName, String resourcePath) {
389             if (moduleName.indexOf('/') >= 0) {
390                 throw new IllegalArgumentException("invalid module name: " + moduleName);
391             }
392             // If the given module name is 'modules', then 'isResource()'
393             // returns false to prevent false positives.
394             ImageLocation loc = findLocation(moduleName, resourcePath);
395             return loc != null && isResource(loc);
396         }
397 
398         /**
399          * Builds a node in the "/modules/..." namespace.
400          *
401          * <p>Called by {@link #findNode(String)} if a {@code /modules/...} node
402          * is not present in the cache.
403          */
404         private Node buildModulesNode(String name) {
405             assert name.startsWith(MODULES_ROOT + "/") : "Invalid module node name: " + name;
406             // Returns null for non-directory resources, since the jimage name does not
407             // start with "/modules" (e.g. "/java.base/java/lang/Object.class").
408             ImageLocation loc = findLocation(name);
409             if (loc != null) {
410                 assert name.equals(loc.getFullName()) : "Mismatched location for directory: " + name;
411                 assert isModulesSubdirectory(loc) : "Invalid modules directory: " + name;
412                 return completeModuleDirectory(newDirectory(name), loc);
413             }
414             // Now try the non-prefixed resource name, but be careful to avoid false
415             // positives for names like "/modules/modules/xxx" which could return a
416             // location of a directory entry.
417             loc = findLocation(name.substring(MODULES_ROOT.length()));
418             return loc != null && isResource(loc) ? newResource(name, loc) : null;
419         }
420 
421         /**
422          * Builds a node in the "/packages/..." namespace.
423          *
424          * <p>Called by {@link #findNode(String)} if a {@code /packages/...} node
425          * is not present in the cache.
426          */
427         private Node buildPackagesNode(String name) {
428             // There are only locations for the root "/packages" or "/packages/xxx"
429             // directories, but not the symbolic links below them (the links can be
430             // entirely derived from the name information in the parent directory).
431             // However, unlike resources this means that we do not have a constant
432             // time lookup for link nodes when creating them.
433             int packageStart = PACKAGES_ROOT.length() + 1;
434             int packageEnd = name.indexOf('/', packageStart);
435             if (packageEnd == -1) {
436                 ImageLocation loc = findLocation(name);
437                 return loc != null ? completePackageDirectory(newDirectory(name), loc) : null;
438             } else {
439                 // We cannot assume that the parent directory exists for a link node, since
440                 // the given name is untrusted and could reference a non-existent link.
441                 // However, if the parent directory is present, we can conclude that the
442                 // given name was not a valid link (or else it would already be cached).
443                 String dirName = name.substring(0, packageEnd);
444                 if (!nodes.containsKey(dirName)) {
445                     ImageLocation loc = findLocation(dirName);
446                     // If the parent location doesn't exist, the link node cannot exist.
447                     if (loc != null) {
448                         nodes.put(dirName, completePackageDirectory(newDirectory(dirName), loc));
449                         // When the parent is created its child nodes are created and cached,
450                         // but this can still return null if given name wasn't a valid link.
451                         return nodes.get(name);
452                     }
453                 }
454             }
455             return null;
456         }
457 
458         /** Completes a directory by ensuring its child list is populated correctly. */
459         private void completeDirectory(Directory dir) {
460             String name = dir.getName();
461             // Since the node exists, we can assert that its name starts with
462             // either "/modules" or "/packages", making differentiation easy.
463             // It also means that the name is valid, so it must yield a location.
464             assert name.startsWith(MODULES_ROOT) || name.startsWith(PACKAGES_ROOT);
465             ImageLocation loc = findLocation(name);
466             assert loc != null && name.equals(loc.getFullName()) : "Invalid location for name: " + name;
467             // We cannot use 'isXxxSubdirectory()' methods here since we could
468             // be given a top-level directory (for which that test doesn't work).
469             // The string MUST start "/modules" or "/packages" here.
470             if (name.charAt(1) == 'm') {
471                 completeModuleDirectory(dir, loc);
472             } else {
473                 completePackageDirectory(dir, loc);
474             }
475             assert dir.isCompleted() : "Directory must be complete by now: " + dir;
476         }
477 
478         /**
479          * Completes a modules directory by setting the list of child nodes.
480          *
481          * <p>The given directory can be the top level {@code /modules} directory,
482          * so it is NOT safe to use {@code isModulesSubdirectory(loc)} here.
483          */
484         private Directory completeModuleDirectory(Directory dir, ImageLocation loc) {
485             assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir;
486             List<Node> children = createChildNodes(loc, childLoc -> {
487                 if (isModulesSubdirectory(childLoc)) {
488                     return nodes.computeIfAbsent(childLoc.getFullName(), this::newDirectory);
489                 } else {
490                     // Add "/modules" prefix to image location paths to get node names.
491                     String resourceName = childLoc.getFullName(true);
492                     return nodes.computeIfAbsent(resourceName, n -> newResource(n, childLoc));
493                 }
494             });
495             dir.setChildren(children);
496             return dir;
497         }
498 
499         /**
500          * Completes a package directory by setting the list of child nodes.
501          *
502          * <p>The given directory can be the top level {@code /packages} directory,
503          * so it is NOT safe to use {@code isPackagesSubdirectory(loc)} here.
504          */
505         private Directory completePackageDirectory(Directory dir, ImageLocation loc) {
506             assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir;
507             // The only directories in the "/packages" namespace are "/packages" or
508             // "/packages/<package>". However, unlike "/modules" directories, the
509             // location offsets mean different things.
510             List<Node> children;
511             if (dir.getName().equals(PACKAGES_ROOT)) {
512                 // Top-level directory just contains a list of subdirectories.
513                 children = createChildNodes(loc, c -> nodes.computeIfAbsent(c.getFullName(), this::newDirectory));
514             } else {
515                 // A package directory's content is array of offset PAIRS in the
516                 // Strings table, but we only need the 2nd value of each pair.
517                 IntBuffer intBuffer = getOffsetBuffer(loc);
518                 int offsetCount = intBuffer.capacity();
519                 assert (offsetCount & 0x1) == 0 : "Offset count must be even: " + offsetCount;
520                 children = new ArrayList<>(offsetCount / 2);
521                 // Iterate the 2nd offset in each pair (odd indices).
522                 for (int i = 1; i < offsetCount; i += 2) {
523                     String moduleName = getString(intBuffer.get(i));
524                     children.add(nodes.computeIfAbsent(
525                             dir.getName() + "/" + moduleName,
526                             n -> newLinkNode(n, MODULES_ROOT + "/" + moduleName)));
527                 }
528             }
529             // This only happens once and "completes" the directory.
530             dir.setChildren(children);
531             return dir;
532         }
533 
534         /**
535          * Creates the list of child nodes for a {@code Directory} based on a given
536          *
537          * <p>Note: This cannot be used for package subdirectories as they have
538          * child offsets stored differently to other directories.
539          */
540         private List<Node> createChildNodes(ImageLocation loc, Function<ImageLocation, Node> newChildFn) {
541             IntBuffer offsets = getOffsetBuffer(loc);
542             int childCount = offsets.capacity();
543             List<Node> children = new ArrayList<>(childCount);
544             for (int i = 0; i < childCount; i++) {
545                 children.add(newChildFn.apply(getLocation(offsets.get(i))));
546             }
547             return children;
548         }
549 
550         /** Helper to extract the integer offset buffer from a directory location. */
551         private IntBuffer getOffsetBuffer(ImageLocation dir) {
552             assert !isResource(dir) : "Not a directory: " + dir.getFullName();
553             byte[] offsets = getResource(dir);
554             ByteBuffer buffer = ByteBuffer.wrap(offsets);
555             buffer.order(getByteOrder());
556             return buffer.asIntBuffer();
557         }
558 
559         /**
560          * Efficiently determines if an image location is a resource.
561          *
562          * <p>A resource must have a valid module associated with it, so its
563          * module offset must be non-zero, and not equal to the offsets for
564          * "/modules/..." or "/packages/..." entries.
565          */
566         private boolean isResource(ImageLocation loc) {
567             int moduleOffset = loc.getModuleOffset();
568             return moduleOffset != 0
569                     && moduleOffset != modulesStringOffset
570                     && moduleOffset != packagesStringOffset;
571         }
572 
573         /**
574          * Determines if an image location is a directory in the {@code /modules}
575          * namespace (if so, the location name is the node name).
576          *
577          * <p>In jimage, every {@code ImageLocation} under {@code /modules/} is a
578          * directory and has the same value for {@code getModule()}, and {@code
579          * getModuleOffset()}.
580          */
581         private boolean isModulesSubdirectory(ImageLocation loc) {
582             return loc.getModuleOffset() == modulesStringOffset;
583         }
584 
585         /**
586          * Creates an "incomplete" directory node with no child nodes set.
587          * Directories need to be "completed" before they are returned by
588          * {@link #findNode(String)}.
589          */
590         private Directory newDirectory(String name) {
591             return new Directory(name, imageFileAttributes);
592         }
593 
594         /**
595          * Creates a new resource from an image location. This is the only case
596          * where the image location name does not match the requested node name.
597          * In image files, resource locations are NOT prefixed by {@code /modules}.
598          */
599         private Resource newResource(String name, ImageLocation loc) {
600             assert name.equals(loc.getFullName(true)) : "Mismatched location for resource: " + name;
601             return new Resource(name, loc, imageFileAttributes);
602         }
603 
604         /**
605          * Creates a new link node pointing at the given target name.
606          *
607          * <p>Note that target node is resolved each time {@code resolve()} is called,
608          * so if a link node is retained after its reader is closed, it will fail.
609          */
610         private LinkNode newLinkNode(String name, String targetName) {
611             return new LinkNode(name, () -> findNode(targetName), imageFileAttributes);
612         }
613 
614         /** Returns the content of a resource node. */
615         private byte[] getResource(Node node) throws IOException {
616             // We could have been given a non-resource node here.
617             if (node.isResource()) {
618                 return super.getResource(node.getLocation());
619             }
620             throw new IOException("Not a resource: " + node);
621         }
622     }
623 
624     /**
625      * A directory, resource or symbolic link.
626      *
627      * <h3 id="node_equality">Node Equality</h3>
628      *
629      * Nodes are identified solely by their name, and it is not valid to attempt
630      * to compare nodes from different reader instances. Different readers may
631      * produce nodes with the same names, but different contents.
632      *
633      * <p>Furthermore, since a {@link ImageReader} provides "perfect" caching of
634      * nodes, equality of nodes from the same reader is equivalent to instance
635      * identity.
636      */
637     public abstract static class Node {
638         private final String name;
639         private final BasicFileAttributes fileAttrs;
640 
641         /**
642          * Creates an abstract {@code Node}, which is either a resource, directory
643          * or symbolic link.
644          *
645          * <p>This constructor is only non-private so it can be used by the
646          * {@code ExplodedImage} class, and must not be used otherwise.
647          */
648         protected Node(String name, BasicFileAttributes fileAttrs) {
649             this.name = Objects.requireNonNull(name);
650             this.fileAttrs = Objects.requireNonNull(fileAttrs);
651         }
652 
653         // A node is completed when all its direct children have been built.
654         // As such, non-directory nodes are always complete.
655         boolean isCompleted() {
656             return true;
657         }
658 
659         // Only resources can return a location.
660         ImageLocation getLocation() {
661             throw new IllegalStateException("not a resource: " + getName());
662         }
663 
664         /**
665          * Returns the name of this node (e.g. {@code
666          * "/modules/java.base/java/lang/Object.class"} or {@code
667          * "/packages/java.lang"}).
668          *
669          * <p>Note that for resource nodes this is NOT the underlying jimage
670          * resource name (it is prefixed with {@code "/modules"}).
671          */
672         public final String getName() {
673             return name;
674         }
675 
676         /**
677          * Returns file attributes for this node. The value returned may be the
678          * same for all nodes, and should not be relied upon for accuracy.
679          */
680         public final BasicFileAttributes getFileAttributes() {
681             return fileAttrs;
682         }
683 
684         /**
685          * Resolves a symbolic link to its target node. If this code is not a
686          * symbolic link, then it resolves to itself.
687          */
688         public final Node resolveLink() {
689             return resolveLink(false);
690         }
691 
692         /**
693          * Resolves a symbolic link to its target node. If this code is not a
694          * symbolic link, then it resolves to itself.
695          */
696         public Node resolveLink(boolean recursive) {
697             return this;
698         }
699 
700         /** Returns whether this node is a symbolic link. */
701         public boolean isLink() {
702             return false;
703         }
704 
705         /**
706          * Returns whether this node is a directory. Directory nodes can have
707          * {@link #getChildNames()} invoked to get the fully qualified names
708          * of any child nodes.
709          */
710         public boolean isDirectory() {
711             return false;
712         }
713 
714         /**
715          * Returns whether this node is a resource. Resource nodes can have
716          * their contents obtained via {@link ImageReader#getResource(Node)}
717          * or {@link ImageReader#getResourceBuffer(Node)}.
718          */
719         public boolean isResource() {
720             return false;
721         }
722 
723         /**
724          * Returns the fully qualified names of any child nodes for a directory.
725          *
726          * <p>By default, this method throws {@link IllegalStateException} and
727          * is overridden for directories.
728          */
729         public Stream<String> getChildNames() {
730             throw new IllegalStateException("not a directory: " + getName());
731         }
732 
733         /**
734          * Returns the uncompressed size of this node's content. If this node is
735          * not a resource, this method returns zero.
736          */
737         public long size() {
738             return 0L;
739         }
740 
741         /**
742          * Returns the compressed size of this node's content. If this node is
743          * not a resource, this method returns zero.
744          */
745         public long compressedSize() {
746             return 0L;
747         }
748 
749         /**
750          * Returns the extension string of a resource node. If this node is not
751          * a resource, this method returns null.
752          */
753         public String extension() {
754             return null;
755         }
756 
757         @Override
758         public final String toString() {
759             return getName();
760         }
761 
762         /** See <a href="#node_equality">Node Equality</a>. */
763         @Override
764         public final int hashCode() {
765             return name.hashCode();
766         }
767 
768         /** See <a href="#node_equality">Node Equality</a>. */
769         @Override
770         public final boolean equals(Object other) {
771             if (this == other) {
772                 return true;
773             }
774 
775             if (other instanceof Node) {
776                 return name.equals(((Node) other).name);
777             }
778 
779             return false;
780         }
781     }
782 
783     /**
784      * Directory node (referenced from a full path, without a trailing '/').
785      *
786      * <p>Directory nodes have two distinct states:
787      * <ul>
788      *     <li>Incomplete: The child list has not been set.
789      *     <li>Complete: The child list has been set.
790      * </ul>
791      *
792      * <p>When a directory node is returned by {@link ImageReader#findNode(String)}
793      * it is always complete, but this DOES NOT mean that its child nodes are
794      * complete yet.
795      *
796      * <p>To avoid users being able to access incomplete child nodes, the
797      * {@code Node} API offers only a way to obtain child node names, forcing
798      * callers to invoke {@code findNode()} if they need to access the child
799      * node itself.
800      *
801      * <p>This approach allows directories to be implemented lazily with respect
802      * to child nodes, while retaining efficiency when child nodes are accessed
803      * (since any incomplete nodes will be created and placed in the node cache
804      * when the parent was first returned to the user).
805      */
806     private static final class Directory extends Node {
807         // Monotonic reference, will be set to the unmodifiable child list exactly once.
808         private List<Node> children = null;
809 
810         private Directory(String name, BasicFileAttributes fileAttrs) {
811             super(name, fileAttrs);
812         }
813 
814         @Override
815         boolean isCompleted() {
816             return children != null;
817         }
818 
819         @Override
820         public boolean isDirectory() {
821             return true;
822         }
823 
824         @Override
825         public Stream<String> getChildNames() {
826             if (children != null) {
827                 return children.stream().map(Node::getName);
828             }
829             throw new IllegalStateException("Cannot get child nodes of an incomplete directory: " + getName());
830         }
831 
832         private void setChildren(List<Node> children) {
833             assert this.children == null : this + ": Cannot set child nodes twice!";
834             this.children = Collections.unmodifiableList(children);
835         }
836     }
837     /**
838      * Resource node (e.g. a ".class" entry, or any other data resource).
839      *
840      * <p>Resources are leaf nodes referencing an underlying image location. They
841      * are lightweight, and do not cache their contents.
842      *
843      * <p>Unlike directories (where the node name matches the jimage path for the
844      * corresponding {@code ImageLocation}), resource node names are NOT the same
845      * as the corresponding jimage path. The difference is that node names for
846      * resources are prefixed with "/modules", which is missing from the
847      * equivalent jimage path.
848      */
849     private static class Resource extends Node {
850         private final ImageLocation loc;
851 
852         private Resource(String name, ImageLocation loc, BasicFileAttributes fileAttrs) {
853             super(name, fileAttrs);
854             this.loc = loc;
855         }
856 
857         @Override
858         ImageLocation getLocation() {
859             return loc;
860         }
861 
862         @Override
863         public boolean isResource() {
864             return true;
865         }
866 
867         @Override
868         public long size() {
869             return loc.getUncompressedSize();
870         }
871 
872         @Override
873         public long compressedSize() {
874             return loc.getCompressedSize();
875         }
876 
877         @Override
878         public String extension() {
879             return loc.getExtension();
880         }
881     }
882 
883     /**
884      * Link node (a symbolic link to a top-level modules directory).
885      *
886      * <p>Link nodes resolve their target by invoking a given supplier, and do
887      * not cache the result. Since nodes are cached by the {@code ImageReader},
888      * this means that only the first call to {@link #resolveLink(boolean)}
889      * could do any significant work.
890      */
891     private static class LinkNode extends Node {
892         private final Supplier<Node> link;
893 
894         private LinkNode(String name, Supplier<Node> link, BasicFileAttributes fileAttrs) {
895             super(name, fileAttrs);
896             this.link = link;
897         }
898 
899         @Override
900         public Node resolveLink(boolean recursive) {
901             // No need to use or propagate the recursive flag, since the target
902             // cannot possibly be a link node (links only point to directories).
903             return link.get();
904         }
905 
906         @Override
907         public boolean isLink() {
908             return true;
909         }
910     }
911 }