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