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 }