1 /******************************************************************************* 2 * Copyright (c) 2020 Andrey Loskutov and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * Andrey Loskutov <loskutov@gmx.de> - initial API and implementation 13 *******************************************************************************/ 14 package org.eclipse.jdt.internal.compiler.util; 15 16 import java.io.FileNotFoundException; 17 import java.io.IOException; 18 import java.net.URI; 19 import java.nio.file.DirectoryStream; 20 import java.nio.file.FileSystem; 21 import java.nio.file.FileSystemAlreadyExistsException; 22 import java.nio.file.FileSystems; 23 import java.nio.file.Files; 24 import java.nio.file.Path; 25 import java.util.ArrayList; 26 import java.util.Collections; 27 import java.util.HashMap; 28 import java.util.List; 29 import java.util.Map; 30 import java.util.Optional; 31 import java.util.concurrent.ConcurrentHashMap; 32 33 /** 34 * Abstraction to the ct.sym file access (see https://openjdk.java.net/jeps/247). The ct.sym file is required to 35 * implement JEP 247 feature (compile with "--release" option against class stubs for older releases) and is currently 36 * (Java 15) a jar file with undocumented internal structure, currently existing in at least two different format 37 * versions (pre Java 12 and Java 12 and later). 38 * <p> 39 * The only documentation known seem to be the current implementation of 40 * com.sun.tools.javac.platform.JDKPlatformProvider and probably some JDK build tools that construct ct.sym file. Root 41 * directories inside the file are somehow related to the Java release number, encoded as hex (if they contain release 42 * number as hex). 43 * <p> 44 * If a release directory contains "system-modules" file, it is a flag that this release files are not inside ct.sym 45 * file because it is the current release, and jrt file system should be used instead. 46 * <p> 47 * All other release directories contain encoded signature (*.sig) files with class stubs for classes in the release. 48 * <p> 49 * Some directories contain files that are shared between different releases, exact logic how they are distributed is 50 * not known. 51 * <p> 52 * Known format versions of ct.sym: 53 * <p> 54 * Pre JDK 12: 55 * 56 * <pre> 57 * ct.sym -> 9 -> java/ -> lang/ 58 * ct.sym -> 9-modules -> java.base -> module-info.sig 59 * </pre> 60 * 61 * From JDK 12 onward: 62 * 63 * <pre> 64 * ct.sym -> 9 -> java.base -> java/ -> lang/ 65 * ct.sym -> 9 -> java.base -> module-info.sig 66 * </pre> 67 * 68 * Notably, 69 * <ol> 70 * <li>in JDK 12 modules classes and ordinary classes are located in the same location 71 * <li>in JDK 12, ordinary classes are found inside their respective modules 72 * </ol> 73 * <p> 74 * 75 * Searching a file for a given release in ct.sym means finding & traversing all possible release related directories 76 * and searching for matching path. 77 */ 78 public class CtSym { 79 80 public static final boolean DISABLE_CACHE = Boolean.getBoolean("org.eclipse.jdt.disable_CTSYM_cache"); //$NON-NLS-1$ 81 82 static boolean VERBOSE = false; 83 84 /** 85 * Map from path (release) inside ct.sym file to all class signatures loaded 86 */ 87 private final Map<Path, Optional<byte[]>> fileCache = new ConcurrentHashMap<>(10007); 88 89 private final Path jdkHome; 90 91 private final Path ctSymFile; 92 93 private FileSystem fs; 94 95 Path root; 96 97 private boolean isJRE12Plus; 98 99 /** 100 * Paths of all root directories, per release (as hex number). e.g. in JDK 11, Java 10 mapping looks like A -> [A, 101 * A-modules, A789, A9] but to have more fun, in JDK 14, same mapping looks like A -> [A, AB, ABC, ABCD] 102 */ 103 private final Map<String, List<Path>> releaseRootPaths = new ConcurrentHashMap<>(); 104 105 /** 106 * All paths that exist in all release root directories, per release (as hex number). The first key is release 107 * number in hex. The second key is the "full qualified binary name" of the class (without module name and 108 * with .sig suffix). The value is the full path of the corresponding signature file in the ct.sym file. 109 */ 110 private final Map<String, Map<String, Path>> allReleasesPaths = new ConcurrentHashMap<>(); 111 CtSym(Path jdkHome)112 CtSym(Path jdkHome) throws IOException { 113 this.jdkHome = jdkHome; 114 this.ctSymFile = jdkHome.resolve("lib/ct.sym"); //$NON-NLS-1$ 115 init(); 116 } 117 init()118 private void init() throws IOException { 119 boolean exists = Files.exists(this.ctSymFile); 120 if (!exists) { 121 throw new FileNotFoundException("File " + this.ctSymFile + " does not exist"); //$NON-NLS-1$//$NON-NLS-2$ 122 } 123 FileSystem fst = null; 124 URI uri = URI.create("jar:file:" + this.ctSymFile.toUri().getRawPath()); //$NON-NLS-1$ 125 try { 126 fst = FileSystems.getFileSystem(uri); 127 } catch (Exception fne) { 128 // Ignore and move on 129 } 130 if (fst == null) { 131 try { 132 fst = FileSystems.newFileSystem(uri, new HashMap<>()); 133 } catch (FileSystemAlreadyExistsException e) { 134 fst = FileSystems.getFileSystem(uri); 135 } 136 } 137 this.fs = fst; 138 if (fst == null) { 139 throw new IOException("Failed to create ct.sym file system for " + this.ctSymFile); //$NON-NLS-1$ 140 } else { 141 this.root = fst.getPath("/"); //$NON-NLS-1$ 142 this.isJRE12Plus = isCurrentRelease12plus(); 143 } 144 } 145 146 /** 147 * @return never null 148 */ getFs()149 public FileSystem getFs() { 150 return this.fs; 151 } 152 153 /** 154 * 155 * @return true if this file is from Java 12+ JRE 156 */ isJRE12Plus()157 public boolean isJRE12Plus() { 158 return this.isJRE12Plus; 159 } 160 161 /** 162 * @return never null 163 */ getRoot()164 public Path getRoot() { 165 return this.root; 166 } 167 168 /** 169 * @param releaseInHex 170 * major JDK version segment as hex number (8, 9, A, etc) 171 * @return set with all root paths related to given release in ct.sym file 172 */ releaseRoots(String releaseInHex)173 public List<Path> releaseRoots(String releaseInHex) { 174 List<Path> list = this.releaseRootPaths.computeIfAbsent(releaseInHex, x -> { 175 List<Path> rootDirs = new ArrayList<>(); 176 try (DirectoryStream<Path> stream = Files.newDirectoryStream(this.root)) { 177 for (final Path subdir : stream) { 178 String rel = subdir.getFileName().toString(); 179 if (rel.contains("-")) { //$NON-NLS-1$ 180 // Ignore META-INF etc. We are only interested in A-F 0-9 181 continue; 182 } 183 // Line below looks crazy. Latest with release 24 (hex 18) 184 // we will find "8" release paths inside all release 24 related 185 // directories and with release 26 (hex 1A) we will match "10" release 186 // paths inside release 24 directories. I can't believe this is sane. 187 // But looks like similar code is in 188 // com.sun.tools.javac.platform.JDKPlatformProvider.PlatformDescriptionImpl.getFileManager() 189 // https://github.com/openjdk/jdk/blob/master/src/jdk.compiler/share/classes/com/sun/tools/javac/platform/JDKPlatformProvider.java 190 if (rel.contains(releaseInHex)) { 191 rootDirs.add(subdir); 192 } else { 193 continue; 194 } 195 } 196 } catch (IOException e) { 197 return Collections.emptyList(); 198 } 199 return Collections.unmodifiableList(rootDirs); 200 }); 201 return list; 202 } 203 204 /** 205 * Retrieves the full path in ct.sym file fro given signature file in given release 206 * <p> 207 * 12+: something like 208 * <p> 209 * java/io/Reader.sig -> /879/java.base/java/io/Reader.sig 210 * <p> 211 * before 12: 212 * <p> 213 * java/io/Reader.sig -> /8769/java/io/Reader.sig 214 * 215 * @param releaseInHex release number in hex 216 * @param qualifiedSignatureFileName signature file name (without module) 217 * @param moduleName 218 * @return corresponding path in ct.sym file system or null if not found 219 */ getFullPath(String releaseInHex, String qualifiedSignatureFileName, String moduleName)220 public Path getFullPath(String releaseInHex, String qualifiedSignatureFileName, String moduleName) { 221 String sep = this.fs.getSeparator(); 222 if (DISABLE_CACHE) { 223 List<Path> releaseRoots = releaseRoots(releaseInHex); 224 for (Path rroot : releaseRoots) { 225 // Calculate file path 226 Path p = null; 227 if (isJRE12Plus()) { 228 if (moduleName == null) { 229 moduleName = getModuleInJre12plus(releaseInHex, qualifiedSignatureFileName); 230 } 231 p = rroot.resolve(moduleName + sep + qualifiedSignatureFileName); 232 } else { 233 p = rroot.resolve(qualifiedSignatureFileName); 234 } 235 236 // If file is known, read it from ct.sym 237 if (Files.exists(p)) { 238 if (VERBOSE) { 239 System.out.println("found: " + qualifiedSignatureFileName + " in " + p + " for module " + moduleName + "\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ 240 } 241 return p; 242 } 243 } 244 if (VERBOSE) { 245 System.out.println("not found: " + qualifiedSignatureFileName + " for module " + moduleName); //$NON-NLS-1$ //$NON-NLS-2$ 246 } 247 return null; 248 } 249 Map<String, Path> releasePaths = getCachedReleasePaths(releaseInHex); 250 Path path; 251 if(moduleName != null) { 252 // Without this, org.eclipse.jdt.core.tests.model.ModuleBuilderTests.testConvertToModule() fails on 12+ JRE 253 path = releasePaths.get(moduleName + sep + qualifiedSignatureFileName); 254 255 // Special handling of broken module shema in java 11 for compilation with --release 10 256 if(path == null && !this.isJRE12Plus() && "A".equals(releaseInHex)){ //$NON-NLS-1$ 257 path = releasePaths.get(qualifiedSignatureFileName); 258 } 259 } else { 260 path = releasePaths.get(qualifiedSignatureFileName); 261 } 262 if (VERBOSE) { 263 if (path != null) { 264 System.out.println("found: " + qualifiedSignatureFileName + " in " + path + " for module " + moduleName +"\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ 265 } else { 266 System.out.println("not found: " + qualifiedSignatureFileName + " for module " + moduleName); //$NON-NLS-1$ //$NON-NLS-2$ 267 } 268 } 269 return path; 270 } 271 getModuleInJre12plus(String releaseInHex, String qualifiedSignatureFileName)272 private String getModuleInJre12plus(String releaseInHex, String qualifiedSignatureFileName) { 273 if (DISABLE_CACHE) { 274 return findModuleForFileInJre12plus(releaseInHex, qualifiedSignatureFileName); 275 } 276 Map<String, Path> releasePaths = getCachedReleasePaths(releaseInHex); 277 Path path = releasePaths.get(qualifiedSignatureFileName); 278 if (path != null && path.getNameCount() > 2) { 279 // First segment is release, second: module 280 return path.getName(1).toString(); 281 } 282 return null; 283 } 284 findModuleForFileInJre12plus(String releaseInHex, String qualifiedSignatureFileName)285 private String findModuleForFileInJre12plus(String releaseInHex, String qualifiedSignatureFileName) { 286 for (Path rroot : releaseRoots(releaseInHex)) { 287 try (DirectoryStream<Path> stream = Files.newDirectoryStream(rroot)) { 288 for (final Path subdir : stream) { 289 Path p = subdir.resolve(qualifiedSignatureFileName); 290 if (Files.exists(p)) { 291 if (subdir.getNameCount() == 2) { 292 return subdir.getName(1).toString(); 293 } 294 } 295 } 296 } catch (IOException e) { 297 // not found... 298 } 299 } 300 return null; 301 } 302 303 /** 304 * Populates {@link #allReleasesPaths} with the paths of all files within each matching release directory in ct.sym. 305 * This cache is an optimization to avoid excessive calls into the zip filesystem in 306 * {@code ClasspathJrtWithReleaseOption#findClass(String, String, String, String, boolean, Predicate)}. 307 * <p> 308 * 12+: something like 309 * <p> 310 * java.base/javax/net/ssl/SSLSocketFactory.sig -> /89ABC/java.base/javax/net/ssl/SSLSocketFactory.sig 311 * <p> or 312 * javax/net/ssl/SSLSocketFactory.sig -> /89ABC/java.base/javax/net/ssl/SSLSocketFactory.sig 313 * <p> 314 * before 12: javax/net/ssl/SSLSocketFactory.sig -> /89ABC/java.base/javax/net/ssl/SSLSocketFactory.sig 315 */ getCachedReleasePaths(String releaseInHex)316 private Map<String, Path> getCachedReleasePaths(String releaseInHex) { 317 Map<String, Path> result = this.allReleasesPaths.computeIfAbsent(releaseInHex, x -> { 318 List<Path> roots = releaseRoots(releaseInHex); 319 Map<String, Path> allReleaseFiles = new HashMap<>(4999); 320 for (Path start : roots) { 321 try { 322 Files.walk(start).filter(Files::isRegularFile).forEach(p -> { 323 if (isJRE12Plus()) { 324 // Don't use module name as part of the key 325 String binaryNameWithoutModule = p.subpath(2, p.getNameCount()).toString(); 326 allReleaseFiles.put(binaryNameWithoutModule, p); 327 // Cache extra key with module added, see getFullPath(). 328 String binaryNameWithModule = p.subpath(1, p.getNameCount()).toString(); 329 allReleaseFiles.put(binaryNameWithModule, p); 330 } else { 331 String binaryNameWithoutModule = p.subpath(1, p.getNameCount()).toString(); 332 allReleaseFiles.put(binaryNameWithoutModule, p); 333 } 334 }); 335 } catch (IOException e) { 336 // Not much do to if we can't list the dir; anything in there will be treated 337 // as if it were missing. 338 } 339 } 340 return Collections.unmodifiableMap(allReleaseFiles); 341 }); 342 return result; 343 } 344 getFileBytes(Path path)345 public byte[] getFileBytes(Path path) throws IOException { 346 if (DISABLE_CACHE) { 347 return JRTUtil.safeReadBytes(path); 348 } else { 349 Optional<byte[]> bytes = this.fileCache.computeIfAbsent(path, key -> { 350 try { 351 return Optional.ofNullable(JRTUtil.safeReadBytes(key)); 352 } catch (IOException e) { 353 return Optional.empty(); 354 } 355 }); 356 if (VERBOSE) { 357 System.out.println("got bytes: " + path); //$NON-NLS-1$ 358 } 359 return bytes.orElse(null); 360 } 361 } 362 isCurrentRelease12plus()363 private boolean isCurrentRelease12plus() throws IOException { 364 try (DirectoryStream<Path> stream = Files.newDirectoryStream(this.root)) { 365 for (final Path subdir : stream) { 366 String rel = JRTUtil.sanitizedFileName(subdir); 367 if (rel.contains("-")) { //$NON-NLS-1$ 368 continue; 369 } 370 try { 371 int version = Integer.parseInt(rel, 16); 372 // If a release directory contains "system-modules" file, it is a flag 373 // that this is the *current* release 374 if (version > 11 && Files.exists(this.fs.getPath(rel, "system-modules"))) { //$NON-NLS-1$ 375 return true; 376 } 377 } catch (NumberFormatException e) { 378 // META-INF, A-modules etc 379 continue; 380 } 381 } 382 } 383 return false; 384 } 385 386 @Override hashCode()387 public int hashCode() { 388 return this.jdkHome.hashCode(); 389 } 390 391 @Override equals(Object obj)392 public boolean equals(Object obj) { 393 if (this == obj) { 394 return true; 395 } 396 if (!(obj instanceof CtSym)) { 397 return false; 398 } 399 CtSym other = (CtSym) obj; 400 return this.jdkHome.equals(other.jdkHome); 401 } 402 403 @Override toString()404 public String toString() { 405 StringBuilder sb = new StringBuilder(); 406 sb.append("CtSym ["); //$NON-NLS-1$ 407 sb.append("file="); //$NON-NLS-1$ 408 sb.append(this.ctSymFile); 409 sb.append("]"); //$NON-NLS-1$ 410 return sb.toString(); 411 } 412 }