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.FileSystem; 31 import java.nio.file.FileSystemException; 32 import java.nio.file.FileSystems; 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.Collections; 38 import java.util.HashMap; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.stream.Stream; 42 43 import jdk.internal.jimage.ImageReader.Node; 44 45 /** 46 * A jrt file system built on $JAVA_HOME/modules directory ('exploded modules 47 * build') 48 * 49 * @implNote This class needs to maintain JDK 8 source compatibility. 50 * 51 * It is used internally in the JDK to implement jimage/jrtfs access, 52 * but also compiled and delivered as part of the jrtfs.jar to support access 53 * to the jimage file provided by the shipped JDK by tools running on JDK 8. 54 */ 55 class ExplodedImage extends SystemImage { 56 57 private static final String MODULES = "/modules/"; 58 private static final String PACKAGES = "/packages/"; 59 private static final int PACKAGES_LEN = PACKAGES.length(); 60 61 private final FileSystem defaultFS; 62 private final String separator; 63 private final Map<String, PathNode> nodes = Collections.synchronizedMap(new HashMap<>()); 64 private final BasicFileAttributes modulesDirAttrs; 65 66 ExplodedImage(Path modulesDir) throws IOException { 67 defaultFS = FileSystems.getDefault(); 68 String str = defaultFS.getSeparator(); 69 separator = str.equals("/") ? null : str; 70 modulesDirAttrs = Files.readAttributes(modulesDir, BasicFileAttributes.class); 71 initNodes(); 72 } 73 74 // A Node that is backed by actual default file system Path 75 private final class PathNode extends Node { 76 77 // Path in underlying default file system 78 private Path path; 79 private PathNode link; 80 private List<Node> children; 81 82 PathNode(String name, Path path, BasicFileAttributes attrs) { // path 83 super(name, attrs); 84 this.path = path; 85 } 86 87 PathNode(String name, Node link) { // link 88 super(name, link.getFileAttributes()); 89 this.link = (PathNode)link; 90 } 91 92 PathNode(String name, List<Node> children) { // dir 93 super(name, modulesDirAttrs); 94 this.children = children; 95 } 96 97 @Override 98 public boolean isDirectory() { 99 return children != null || 100 (link == null && getFileAttributes().isDirectory()); 101 } 102 103 @Override 104 public boolean isLink() { 105 return link != null; 106 } 107 108 @Override 109 public PathNode resolveLink(boolean recursive) { 110 if (link == null) 111 return this; 112 return recursive && link.isLink() ? link.resolveLink(true) : link; 113 } 114 115 byte[] getContent() throws IOException { 116 if (!getFileAttributes().isRegularFile()) 117 throw new FileSystemException(getName() + " is not file"); 118 return Files.readAllBytes(path); 119 } 120 121 @Override 122 public Stream<String> getChildNames() { 123 if (!isDirectory()) 124 throw new IllegalArgumentException("not a directory: " + getName()); 125 if (children == null) { 126 List<Node> list = new ArrayList<>(); 127 try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) { 128 for (Path p : stream) { 129 p = explodedModulesDir.relativize(p); 130 String pName = MODULES + nativeSlashToFrontSlash(p.toString()); 131 Node node = findNode(pName); 132 if (node != null) { // findNode may choose to hide certain files! 133 list.add(node); 134 } 135 } 136 } catch (IOException x) { 137 return null; 138 } 139 children = list; 140 } 141 return children.stream().map(Node::getName); 142 } 143 144 @Override 145 public long size() { 146 try { 147 return isDirectory() ? 0 : Files.size(path); 148 } catch (IOException ex) { 149 throw new UncheckedIOException(ex); 150 } 151 } 152 } 153 154 @Override 155 public void close() throws IOException { 156 nodes.clear(); 157 } 158 159 @Override 160 public byte[] getResource(Node node) throws IOException { 161 return ((PathNode)node).getContent(); 162 } 163 164 // find Node for the given Path 165 @Override 166 public synchronized Node findNode(String str) { 167 Node node = findModulesNode(str); 168 if (node != null) { 169 return node; 170 } 171 // lazily created for paths like /packages/<package>/<module>/xyz 172 // For example /packages/java.lang/java.base/java/lang/ 173 if (str.startsWith(PACKAGES)) { 174 // pkgEndIdx marks end of <package> part 175 int pkgEndIdx = str.indexOf('/', PACKAGES_LEN); 176 if (pkgEndIdx != -1) { 177 // modEndIdx marks end of <module> part 178 int modEndIdx = str.indexOf('/', pkgEndIdx + 1); 179 if (modEndIdx != -1) { 180 // make sure we have such module link! 181 // ie., /packages/<package>/<module> is valid 182 Node linkNode = nodes.get(str.substring(0, modEndIdx)); 183 if (linkNode == null || !linkNode.isLink()) { 184 return null; 185 } 186 // map to "/modules/zyz" path and return that node 187 // For example, "/modules/java.base/java/lang" for 188 // "/packages/java.lang/java.base/java/lang". 189 String mod = MODULES + str.substring(pkgEndIdx + 1); 190 return findModulesNode(mod); 191 } 192 } 193 } 194 return null; 195 } 196 197 // find a Node for a path that starts like "/modules/..." 198 Node findModulesNode(String str) { 199 PathNode node = nodes.get(str); 200 if (node != null) { 201 return node; 202 } 203 // lazily created "/modules/xyz/abc/" Node 204 // This is mapped to default file system path "<JDK_MODULES_DIR>/xyz/abc" 205 Path p = underlyingPath(str); 206 if (p != null) { 207 try { 208 BasicFileAttributes attrs = Files.readAttributes(p, BasicFileAttributes.class); 209 if (attrs.isRegularFile()) { 210 Path f = p.getFileName(); 211 if (f.toString().startsWith("_the.")) 212 return null; 213 } 214 node = new PathNode(str, p, attrs); 215 nodes.put(str, node); 216 return node; 217 } catch (IOException x) { 218 // does not exists or unable to determine 219 } 220 } 221 return null; 222 } 223 224 Path underlyingPath(String str) { 225 if (str.startsWith(MODULES)) { 226 str = frontSlashToNativeSlash(str.substring("/modules".length())); 227 return defaultFS.getPath(explodedModulesDir.toString(), str); 228 } 229 return null; 230 } 231 232 // convert "/" to platform path separator 233 private String frontSlashToNativeSlash(String str) { 234 return separator == null ? str : str.replace("/", separator); 235 } 236 237 // convert platform path separator to "/" 238 private String nativeSlashToFrontSlash(String str) { 239 return separator == null ? str : str.replace(separator, "/"); 240 } 241 242 // convert "/"s to "."s 243 private String slashesToDots(String str) { 244 return str.replace(separator != null ? separator : "/", "."); 245 } 246 247 // initialize file system Nodes 248 private void initNodes() throws IOException { 249 // same package prefix may exist in multiple modules. This Map 250 // is filled by walking "jdk modules" directory recursively! 251 Map<String, List<String>> packageToModules = new HashMap<>(); 252 try (DirectoryStream<Path> stream = Files.newDirectoryStream(explodedModulesDir)) { 253 for (Path module : stream) { 254 if (Files.isDirectory(module)) { 255 String moduleName = module.getFileName().toString(); 256 // make sure "/modules/<moduleName>" is created 257 findModulesNode(MODULES + moduleName); 258 try (Stream<Path> contentsStream = Files.walk(module)) { 259 contentsStream.filter(Files::isDirectory).forEach((p) -> { 260 p = module.relativize(p); 261 String pkgName = slashesToDots(p.toString()); 262 // skip META-INF and empty strings 263 if (!pkgName.isEmpty() && !pkgName.startsWith("META-INF")) { 264 List<String> moduleNames = packageToModules.get(pkgName); 265 if (moduleNames == null) { 266 moduleNames = new ArrayList<>(); 267 packageToModules.put(pkgName, moduleNames); 268 } 269 moduleNames.add(moduleName); 270 } 271 }); 272 } 273 } 274 } 275 } 276 // create "/modules" directory 277 // "nodes" map contains only /modules/<foo> nodes only so far and so add all as children of /modules 278 PathNode modulesDir = new PathNode("/modules", new ArrayList<>(nodes.values())); 279 nodes.put(modulesDir.getName(), modulesDir); 280 281 // create children under "/packages" 282 List<Node> packagesChildren = new ArrayList<>(packageToModules.size()); 283 for (Map.Entry<String, List<String>> entry : packageToModules.entrySet()) { 284 String pkgName = entry.getKey(); 285 List<String> moduleNameList = entry.getValue(); 286 List<Node> moduleLinkNodes = new ArrayList<>(moduleNameList.size()); 287 for (String moduleName : moduleNameList) { 288 Node moduleNode = findModulesNode(MODULES + moduleName); 289 PathNode linkNode = new PathNode(PACKAGES + pkgName + "/" + moduleName, moduleNode); 290 nodes.put(linkNode.getName(), linkNode); 291 moduleLinkNodes.add(linkNode); 292 } 293 PathNode pkgDir = new PathNode(PACKAGES + pkgName, moduleLinkNodes); 294 nodes.put(pkgDir.getName(), pkgDir); 295 packagesChildren.add(pkgDir); 296 } 297 // "/packages" dir 298 PathNode packagesDir = new PathNode("/packages", packagesChildren); 299 nodes.put(packagesDir.getName(), packagesDir); 300 301 // finally "/" dir! 302 List<Node> rootChildren = new ArrayList<>(); 303 rootChildren.add(packagesDir); 304 rootChildren.add(modulesDir); 305 PathNode root = new PathNode("/", rootChildren); 306 nodes.put(root.getName(), root); 307 } 308 }