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