1 /*
2 * Copyright (c) 2014, 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.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24 import jdk.internal.jimage.BasicImageReader;
25 import jtreg.SkippedException;
26
27 import java.io.IOException;
28 import java.io.UncheckedIOException;
29 import java.net.URI;
30 import java.nio.file.FileSystem;
31 import java.nio.file.FileSystems;
32 import java.nio.file.Files;
33 import java.nio.file.Path;
34 import java.util.Arrays;
35 import java.util.Deque;
36 import java.util.HashSet;
37 import java.util.List;
38 import java.util.Set;
39 import java.util.concurrent.ConcurrentLinkedDeque;
40 import java.util.concurrent.ExecutorService;
41 import java.util.concurrent.Executors;
42 import java.util.concurrent.TimeUnit;
43 import java.util.concurrent.atomic.AtomicInteger;
44 import java.util.stream.Collectors;
45 import java.util.stream.Stream;
46 import java.util.stream.StreamSupport;
47
48 import static java.util.stream.Collectors.joining;
49
50 /*
51 * @test id=load
52 * @summary Load all classes defined in JRT file system.
53 * @library /test/lib
54 * @modules java.base/jdk.internal.jimage
55 * @run main/othervm --add-modules ALL-SYSTEM VerifyJimage
56 */
57
58 /*
59 * @test id=compare
60 * @summary Compare an exploded directory of module classes with the system jimage.
61 * @library /test/lib
62 * @modules java.base/jdk.internal.jimage
63 * @run main/othervm --add-modules ALL-SYSTEM -Djdk.test.threads=10 VerifyJimage ../../jdk/modules
64 */
65 public abstract class VerifyJimage implements Runnable {
66 private static final String MODULE_INFO = "module-info.class";
67
68 public static void main(String... args) throws Exception {
69 // Best practice is to read "test.jdk" in preference to "java.home".
70 String testJdk = System.getProperty("test.jdk", System.getProperty("java.home"));
71 Path jdkRoot = Path.of(testJdk);
72 Path bootimagePath = jdkRoot.resolve("lib", "modules");
73 if (Files.notExists(bootimagePath)) {
74 throw new SkippedException("No boot image: " + bootimagePath);
75 }
76
77 FileSystem jrtFs = FileSystems.getFileSystem(URI.create("jrt:/"));
78 Path modulesRoot = jrtFs.getPath("/").resolve("modules");
79 List<String> modules;
80 try (Stream<Path> moduleDirs = Files.list(modulesRoot)) {
81 modules = moduleDirs.map(Path::getFileName).map(Object::toString).toList();
82 }
83 VerifyJimage verifier;
84 if (args.length == 0) {
85 verifier = new ClassLoadingVerifier(modules, modulesRoot);
86 } else {
87 Path pathArg = Path.of(args[0].replace("/", FileSystems.getDefault().getSeparator()));
88 // The path argument may be relative.
89 Path rootDir = jdkRoot.resolve(pathArg);
90 if (!Files.isDirectory(rootDir)) {
91 throw new SkippedException("No modules directory found: " + rootDir);
92 }
93 int maxThreads = Integer.getInteger("jdk.test.threads", 1);
94 verifier = new DirectoryContentVerifier(modules, rootDir, maxThreads, bootimagePath);
95 }
96 verifier.verify();
97 }
98
99 final List<String> modules;
100 // Count of items which have passed verification.
101 final AtomicInteger verifiedCount = new AtomicInteger(0);
102 // Error messages for verification failures.
103 final Deque<String> failed = new ConcurrentLinkedDeque<>();
104
105 private VerifyJimage(List<String> modules) {
106 this.modules = modules;
107 }
108
109 void verify() {
110 long start = System.nanoTime();
111 run();
112 long end = System.nanoTime();
113
114 System.out.format("Verified %d entries: %d ms, %d errors%n",
115 verifiedCount.get(),
116 TimeUnit.NANOSECONDS.toMillis(end - start),
117 failed.size());
118 if (!failed.isEmpty()) {
119 failed.forEach(System.err::println);
120 throw new AssertionError("Test failed");
121 }
122 }
123
124 private static final class DirectoryContentVerifier extends VerifyJimage {
125 private final Path rootDir;
126 private final ExecutorService pool;
127 private final Path jimagePath;
128
129 DirectoryContentVerifier(List<String> modules, Path rootDir, int maxThreads, Path jimagePath) {
130 super(modules);
131 this.rootDir = rootDir;
132 this.pool = Executors.newFixedThreadPool(maxThreads);
133 this.jimagePath = jimagePath;
134 }
135
136 @Override
137 public void run() {
138 System.out.println("Comparing jimage with: " + rootDir);
139 try (BasicImageReader jimage = BasicImageReader.open(jimagePath)) {
140 for (String modName : modules) {
141 Path modDir = rootDir.resolve(modName);
142 if (!Files.isDirectory(modDir)) {
143 failed.add("Missing module directory: " + modDir);
144 } else {
145 pool.execute(new ModuleResourceComparator(rootDir, modName, jimage));
146 }
147 }
148 pool.shutdown();
149 if (!pool.awaitTermination(20, TimeUnit.SECONDS)) {
150 failed.add("Directory verification timed out");
151 }
152 } catch (IOException ex) {
153 throw new UncheckedIOException(ex);
154 } catch (InterruptedException e) {
155 failed.add("Directory verification was interrupted");
156 Thread.currentThread().interrupt();
157 }
158 }
159
160 /**
161 * Verifies the contents of the current runtime jimage file by comparing
162 * entries with the on-disk resources in a given directory.
163 */
164 private class ModuleResourceComparator implements Runnable {
165 private final Path rootDir;
166 private final String moduleName;
167 private final BasicImageReader jimage;
168 private final String moduleInfoName;
169 // Entries we expect to find in the jimage module.
170 private final Set<String> moduleEntries;
171 private final Set<String> handledEntries = new HashSet<>();
172
173 public ModuleResourceComparator(Path rootDir, String moduleName, BasicImageReader jimage) {
174 this.rootDir = rootDir;
175 this.moduleName = moduleName;
176 this.jimage = jimage;
177 String moduleEntryPrefix = "/" + moduleName + "/";
178 this.moduleInfoName = moduleEntryPrefix + MODULE_INFO;
179 this.moduleEntries =
180 Arrays.stream(jimage.getEntryNames())
181 .filter(n -> n.startsWith(moduleEntryPrefix))
182 .filter(n -> !isJimageOnly(n))
183 .collect(Collectors.toSet());
184 }
185
186 @Override
187 public void run() {
188 try (Stream<Path> files = Files.walk(rootDir.resolve(moduleName))) {
189 files.filter(this::shouldVerify).forEach(this::compareEntry);
190 } catch (IOException e) {
191 throw new UncheckedIOException(e);
192 }
193 moduleEntries.stream()
194 .filter(n -> !handledEntries.contains(n))
195 .sorted()
196 .forEach(n -> failed.add("Untested jimage entry: " + n));
197 }
198
199 void compareEntry(Path path) {
200 String entryName = getEntryName(path);
201 if (!moduleEntries.contains(entryName)) {
202 // Corresponds to an on-disk file which is not expected to
203 // be present in the jimage. This is normal and is skipped.
204 return;
205 }
206 // Mark valid entries as "handled" to track if we've seen them
207 // (even if we don't test their content).
208 if (!handledEntries.add(entryName)) {
209 failed.add("Duplicate entry name: " + entryName);
210 return;
211 }
212 if (isExpectedToDiffer(entryName)) {
213 return;
214 }
215 try {
216 int mismatch = Arrays.mismatch(
217 Files.readAllBytes(path),
218 jimage.getResource(entryName));
219 if (mismatch == -1) {
220 verifiedCount.incrementAndGet();
221 } else {
222 failed.add("Content diff (byte offset " + mismatch + "): " + entryName);
223 }
224 } catch (IOException e) {
225 throw new UncheckedIOException(e);
226 }
227 }
228
229 /**
230 * Predicate for files which correspond to entries in the jimage.
231 *
232 * <p>This should be a narrow test with minimal chance of
233 * false-negative matching, primarily focusing on excluding build
234 * artifacts.
235 */
236 boolean shouldVerify(Path path) {
237 // Use the entry name because we know it uses the '/' separator.
238 String entryName = getEntryName(path);
239 return Files.isRegularFile(path)
240 && !entryName.contains("/_the.")
241 && !entryName.contains("/_element_lists.");
242 }
243
244 /**
245 * Predicate for the limited subset of entries which are expected to
246 * exist in the file system, but are not expected to have the same
247 * content as the associated jimage entry. This is to handle files
248 * which are modified/patched by jlink plugins.
249 *
250 * <p>This should be a narrow test with minimal chance of
251 * false-positive matching.
252 */
253 private boolean isExpectedToDiffer(String entryName) {
254 return entryName.equals(moduleInfoName)
255 || (entryName.startsWith("/java.base/java/lang/invoke/") && entryName.endsWith("$Holder.class"))
256 || entryName.equals("/java.base/jdk/internal/module/SystemModulesMap.class");
257 }
258
259 /**
260 * Predicate for the limited subset of entries which are not expected
261 * to exist in the file system, such as those created synthetically
262 * by jlink plugins.
263 *
264 * <p>This should be a narrow test with minimal chance of
265 * false-positive matching.
266 */
267 private boolean isJimageOnly(String entryName) {
268 return entryName.startsWith("/java.base/jdk/internal/module/SystemModules$")
269 || entryName.startsWith("/java.base/java/lang/invoke/BoundMethodHandle$Species_");
270 }
271
272 private String getEntryName(Path path) {
273 return StreamSupport.stream(rootDir.relativize(path).spliterator(), false)
274 .map(Object::toString).collect(joining("/", "/", ""));
275 }
276 }
277 }
278
279 /**
280 * Verifies the contents of the current runtime jimage file by attempting to
281 * load every available class based on the content of the JRT file system.
282 */
283 static final class ClassLoadingVerifier extends VerifyJimage {
284 private static final String CLASS_SUFFIX = ".class";
285
286 private final Path modulesRoot;
287
288 ClassLoadingVerifier(List<String> modules, Path modulesRoot) {
289 super(modules);
290 this.modulesRoot = modulesRoot;
291 }
292
293 @Override
294 public void run() {
295 ClassLoader loader = ClassLoader.getSystemClassLoader();
296 for (String modName : modules) {
297 Path modDir = modulesRoot.resolve(modName);
298 try (Stream<Path> files = Files.walk(modDir)) {
299 files.map(modDir::relativize)
300 .filter(ClassLoadingVerifier::isClassFile)
301 .map(ClassLoadingVerifier::toClassName)
302 .forEach(cn -> loadClass(cn, loader));
303 } catch (IOException ex) {
304 throw new UncheckedIOException(ex);
305 }
306 }
307 }
308
309 private void loadClass(String cn, ClassLoader loader) {
310 try {
311 Class.forName(cn, false, loader);
312 verifiedCount.incrementAndGet();
313 } catch (VerifyError ve) {
314 System.err.println("VerifyError for " + cn);
315 failed.add("Class: " + cn + " not verified: " + ve.getMessage());
316 } catch (ClassNotFoundException e) {
317 failed.add("Class: " + cn + " not found");
318 }
319 }
320
321 /**
322 * Maps a module-relative JRT path of a class file to its corresponding
323 * fully-qualified class name.
324 */
325 private static String toClassName(Path path) {
326 // JRT uses '/' as the separator, and relative paths don't start with '/'.
327 String s = path.toString();
328 return s.substring(0, s.length() - CLASS_SUFFIX.length()).replace('/', '.');
329 }
330
331 /** Whether a module-relative JRT file system path is a class file. */
332 private static boolean isClassFile(Path path) {
333 String classFileName = path.getFileName().toString();
334 return classFileName.endsWith(CLASS_SUFFIX)
335 && !classFileName.equals(MODULE_INFO);
336 }
337 }
338 }