1 /* 2 * Copyright (c) 2015, 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.jrtfs; 26 27 import java.io.IOException; 28 import java.io.UncheckedIOException; 29 import java.nio.file.DirectoryStream; 30 import java.nio.file.FileSystemException; 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.HashMap; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.Objects; 39 import java.util.stream.Stream; 40 41 import jdk.internal.jimage.ImageReader.Node; 42 43 /** 44 * A jrt file system built on $JAVA_HOME/modules directory ('exploded modules 45 * build') 46 * 47 * @implNote This class needs to maintain JDK 8 source compatibility. 48 * 49 * It is used internally in the JDK to implement jimage/jrtfs access, 50 * but also compiled and delivered as part of the jrtfs.jar to support access 51 * to the jimage file provided by the shipped JDK by tools running on JDK 8. 52 */ 53 class ExplodedImage extends SystemImage { 54 55 private static final String MODULES = "/modules/"; 56 private static final String PACKAGES = "/packages/"; 57 58 private final Path modulesDir; 59 private final String separator; 60 private final Map<String, PathNode> nodes = new HashMap<>(); 61 private final BasicFileAttributes modulesDirAttrs; 62 63 ExplodedImage(Path modulesDir) throws IOException { 64 this.modulesDir = modulesDir; 65 String str = modulesDir.getFileSystem().getSeparator(); 66 separator = str.equals("/") ? null : str; 67 modulesDirAttrs = Files.readAttributes(modulesDir, BasicFileAttributes.class); 68 initNodes(); 69 } 70 71 // A Node that is backed by actual default file system Path 72 private final class PathNode extends Node { 73 74 // Path in underlying default file system 75 private Path path; 76 private PathNode link; 77 private List<Node> children; 78 79 private PathNode(String name, Path path, BasicFileAttributes attrs) { // path 80 super(name, attrs); 81 this.path = path; 82 } 83 84 private PathNode(String name, Node link) { // link 85 super(name, link.getFileAttributes()); 86 this.link = (PathNode)link; 87 } 88 89 private PathNode(String name, List<Node> children) { // dir 90 super(name, modulesDirAttrs); 91 this.children = children; 92 } 93 94 @Override 95 public boolean isResource() { 96 return link == null && !getFileAttributes().isDirectory(); 97 } 98 99 @Override 100 public boolean isDirectory() { 101 return children != null || 102 (link == null && getFileAttributes().isDirectory()); 103 } 104 105 @Override 106 public boolean isLink() { 107 return link != null; 108 } 109 110 @Override 111 public PathNode resolveLink(boolean recursive) { 112 if (link == null) 113 return this; 114 return recursive && link.isLink() ? link.resolveLink(true) : link; 115 } 116 117 private byte[] getContent() throws IOException { 118 if (!getFileAttributes().isRegularFile()) 119 throw new FileSystemException(getName() + " is not file"); 120 return Files.readAllBytes(path); 121 } 122 123 @Override 124 public Stream<String> getChildNames() { 125 if (!isDirectory()) 126 throw new IllegalArgumentException("not a directory: " + getName()); 127 if (children == null) { 128 List<Node> list = new ArrayList<>(); 129 try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) { 130 for (Path p : stream) { 131 p = modulesDir.relativize(p); 132 String pName = MODULES + nativeSlashToFrontSlash(p.toString()); 133 Node node = findNode(pName); 134 if (node != null) { // findNode may choose to hide certain files! 135 list.add(node); 136 } 137 } 138 } catch (IOException x) { 139 return null; 140 } 141 children = list; 142 } 143 return children.stream().map(Node::getName); 144 } 145 146 @Override 147 public long size() { 148 try { 149 return isDirectory() ? 0 : Files.size(path); 150 } catch (IOException ex) { 151 throw new UncheckedIOException(ex); 152 } 153 } 154 } 155 156 @Override 157 public synchronized void close() throws IOException { 158 nodes.clear(); 159 } 160 161 @Override 162 public byte[] getResource(Node node) throws IOException { 163 return ((PathNode)node).getContent(); 164 } 165 166 @Override 167 public synchronized Node findNode(String name) { 168 PathNode node = nodes.get(name); 169 if (node != null) { 170 return node; 171 } 172 // If null, this was not the name of "/modules/..." node, and since all 173 // "/packages/..." nodes were created and cached in advance, the name 174 // cannot reference a valid node. 175 Path path = underlyingModulesPath(name); 176 if (path == null) { 177 return null; 178 } 179 // This can still return null for hidden files. 180 return createModulesNode(name, path); 181 } 182 183 /** 184 * Lazily creates and caches a {@code Node} for the given "/modules/..." name 185 * and corresponding path to a file or directory. 186 * 187 * @param name a resource or directory node name, of the form "/modules/...". 188 * @param path the path of a file for a resource or directory. 189 * @return the newly created and cached node, or {@code null} if the given 190 * path references a file which must be hidden in the node hierarchy. 191 */ 192 private Node createModulesNode(String name, Path path) { 193 assert !nodes.containsKey(name) : "Node must not already exist: " + name; 194 assert isNonEmptyModulesPath(name) : "Invalid modules name: " + name; 195 196 try { 197 // We only know if we're creating a resource of directory when we 198 // look up file attributes, and we only do that once. Thus, we can 199 // only reject "marker files" here, rather than by inspecting the 200 // given name string, since it doesn't apply to directories. 201 BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); 202 if (attrs.isRegularFile()) { 203 Path f = path.getFileName(); 204 if (f.toString().startsWith("_the.")) { 205 return null; 206 } 207 } else if (!attrs.isDirectory()) { 208 return null; 209 } 210 PathNode node = new PathNode(name, path, attrs); 211 nodes.put(name, node); 212 return node; 213 } catch (IOException x) { 214 // Since the path reference a file, any errors should not be ignored. 215 throw new UncheckedIOException(x); 216 } 217 } 218 219 /** 220 * Returns the expected file path for name in the "/modules/..." namespace, 221 * or {@code null} if the name is not in the "/modules/..." namespace or the 222 * path does not reference a file. 223 */ 224 private Path underlyingModulesPath(String name) { 225 if (isNonEmptyModulesPath(name)) { 226 Path path = modulesDir.resolve(frontSlashToNativeSlash(name.substring(MODULES.length()))); 227 return Files.exists(path) ? path : null; 228 } 229 return null; 230 } 231 232 private static boolean isNonEmptyModulesPath(String name) { 233 // Don't just check the prefix, there must be something after it too 234 // (otherwise you end up with an empty string after trimming). 235 return name.startsWith(MODULES) && name.length() > MODULES.length(); 236 } 237 238 // convert "/" to platform path separator 239 private String frontSlashToNativeSlash(String str) { 240 return separator == null ? str : str.replace("/", separator); 241 } 242 243 // convert platform path separator to "/" 244 private String nativeSlashToFrontSlash(String str) { 245 return separator == null ? str : str.replace(separator, "/"); 246 } 247 248 // convert "/"s to "."s 249 private String slashesToDots(String str) { 250 return str.replace(separator != null ? separator : "/", "."); 251 } 252 253 // initialize file system Nodes 254 private void initNodes() throws IOException { 255 // same package prefix may exist in multiple modules. This Map 256 // is filled by walking "jdk modules" directory recursively! 257 Map<String, List<String>> packageToModules = new HashMap<>(); 258 try (DirectoryStream<Path> stream = Files.newDirectoryStream(modulesDir)) { 259 for (Path module : stream) { 260 if (Files.isDirectory(module)) { 261 String moduleName = module.getFileName().toString(); 262 // make sure "/modules/<moduleName>" is created 263 Objects.requireNonNull(createModulesNode(MODULES + moduleName, module)); 264 try (Stream<Path> contentsStream = Files.walk(module)) { 265 contentsStream.filter(Files::isDirectory).forEach((p) -> { 266 p = module.relativize(p); 267 String pkgName = slashesToDots(p.toString()); 268 // skip META-INF and empty strings 269 if (!pkgName.isEmpty() && !pkgName.startsWith("META-INF")) { 270 packageToModules 271 .computeIfAbsent(pkgName, k -> new ArrayList<>()) 272 .add(moduleName); 273 } 274 }); 275 } 276 } 277 } 278 } 279 // create "/modules" directory 280 // "nodes" map contains only /modules/<foo> nodes only so far and so add all as children of /modules 281 PathNode modulesRootNode = new PathNode("/modules", new ArrayList<>(nodes.values())); 282 nodes.put(modulesRootNode.getName(), modulesRootNode); 283 284 // create children under "/packages" 285 List<Node> packagesChildren = new ArrayList<>(packageToModules.size()); 286 for (Map.Entry<String, List<String>> entry : packageToModules.entrySet()) { 287 String pkgName = entry.getKey(); 288 List<String> moduleNameList = entry.getValue(); 289 List<Node> moduleLinkNodes = new ArrayList<>(moduleNameList.size()); 290 for (String moduleName : moduleNameList) { 291 Node moduleNode = Objects.requireNonNull(nodes.get(MODULES + moduleName)); 292 PathNode linkNode = new PathNode(PACKAGES + pkgName + "/" + moduleName, moduleNode); 293 nodes.put(linkNode.getName(), linkNode); 294 moduleLinkNodes.add(linkNode); 295 } 296 PathNode pkgDir = new PathNode(PACKAGES + pkgName, moduleLinkNodes); 297 nodes.put(pkgDir.getName(), pkgDir); 298 packagesChildren.add(pkgDir); 299 } 300 // "/packages" dir 301 PathNode packagesRootNode = new PathNode("/packages", packagesChildren); 302 nodes.put(packagesRootNode.getName(), packagesRootNode); 303 304 // finally "/" dir! 305 List<Node> rootChildren = new ArrayList<>(); 306 rootChildren.add(packagesRootNode); 307 rootChildren.add(modulesRootNode); 308 PathNode root = new PathNode("/", rootChildren); 309 nodes.put(root.getName(), root); 310 } 311 }