1 /*
2 * Copyright (c) 2014, 2023, 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 java.io.File;
25 import java.io.IOException;
26 import java.io.UncheckedIOException;
27 import java.nio.file.DirectoryStream;
28 import java.nio.file.Files;
29 import java.nio.file.Path;
30 import java.nio.file.Paths;
31 import java.nio.file.attribute.BasicFileAttributes;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Deque;
35 import java.util.List;
36 import java.util.Set;
37 import java.util.concurrent.ConcurrentLinkedDeque;
38 import java.util.concurrent.ExecutorService;
39 import java.util.concurrent.Executors;
40 import java.util.concurrent.TimeUnit;
41 import java.util.concurrent.atomic.AtomicInteger;
42 import java.util.stream.Collectors;
43 import java.util.stream.Stream;
44
45 import jdk.internal.jimage.BasicImageReader;
46 import jdk.internal.jimage.ImageLocation;
47
48 /*
49 * @test
50 * @summary Verify jimage
51 * @modules java.base/jdk.internal.jimage
52 * @run main/othervm --add-modules ALL-SYSTEM VerifyJimage
53 */
54
55 /**
56 * This test runs in two modes:
57 * (1) No argument: it verifies the jimage by loading all classes in the runtime
58 * (2) path of exploded modules: it compares bytes of each file in the exploded
59 * module with the entry in jimage
60 *
61 * FIXME: exception thrown when findLocation from jimage by multiple threads
62 * -Djdk.test.threads=<n> to specify the number of threads.
63 */
64 public class VerifyJimage {
65 private static final String MODULE_INFO = "module-info.class";
66 private static final Deque<String> failed = new ConcurrentLinkedDeque<>();
67
68 public static void main(String... args) throws Exception {
69
70 String home = System.getProperty("java.home");
71 Path bootimagePath = Paths.get(home, "lib", "modules");
72 if (Files.notExists(bootimagePath)) {
73 System.out.println("Test skipped, not an images build");
74 return;
75 }
76
77 long start = System.nanoTime();
78 int numThreads = Integer.getInteger("jdk.test.threads", 1);
79 JImageReader reader = newJImageReader();
80 VerifyJimage verify = new VerifyJimage(reader, numThreads);
81 if (args.length == 0) {
82 // load classes from jimage
83 verify.loadClasses();
84 } else {
85 Path dir = Paths.get(args[0]);
86 if (Files.notExists(dir) || !Files.isDirectory(dir)) {
87 throw new RuntimeException("Invalid argument: " + dir);
88 }
89 verify.compareExplodedModules(dir);
90 }
91 verify.waitForCompletion();
92 long end = System.nanoTime();
93 int entries = reader.entries();
94 System.out.format("%d entries %d files verified: %d ms %d errors%n",
95 entries, verify.count.get(),
96 TimeUnit.NANOSECONDS.toMillis(end - start), failed.size());
97 for (String f : failed) {
98 System.err.println(f);
99 }
100 if (!failed.isEmpty()) {
101 throw new AssertionError("Test failed");
102 }
103 }
104
105 private final AtomicInteger count = new AtomicInteger(0);
106 private final JImageReader reader;
107 private final ExecutorService pool;
108
109 VerifyJimage(JImageReader reader, int numThreads) {
110 this.reader = reader;
111 this.pool = Executors.newFixedThreadPool(numThreads);
112 }
113
114 private void waitForCompletion() throws InterruptedException {
115 pool.shutdown();
116 pool.awaitTermination(20, TimeUnit.SECONDS);
117 }
118
119 private void compareExplodedModules(Path dir) throws IOException {
120 System.out.println("comparing jimage with " + dir);
121
122 try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
123 for (Path mdir : stream) {
124 if (Files.isDirectory(mdir)) {
125 pool.execute(new Runnable() {
126 @Override
127 public void run() {
128 try {
129 Files.find(mdir, Integer.MAX_VALUE, (Path p, BasicFileAttributes attr)
130 -> !Files.isDirectory(p) &&
131 !mdir.relativize(p).toString().startsWith("_") &&
132 !p.getFileName().toString().equals("MANIFEST.MF"))
133 .forEach(p -> compare(mdir, p, reader));
134 } catch (IOException e) {
135 throw new UncheckedIOException(e);
136 }
137 }
138 });
139 }
140 }
141 }
142 }
143
144 private final List<String> BOOT_RESOURCES = Arrays.asList(
145 "java.base/META-INF/services/java.nio.file.spi.FileSystemProvider"
146 );
147 private final List<String> EXT_RESOURCES = Arrays.asList(
148 "jdk.zipfs/META-INF/services/java.nio.file.spi.FileSystemProvider"
149 );
150 private final List<String> APP_RESOURCES = Arrays.asList(
151 "jdk.hotspot.agent/META-INF/services/com.sun.jdi.connect.Connector",
152 "jdk.jdi/META-INF/services/com.sun.jdi.connect.Connector"
153 );
154
155 private void compare(Path mdir, Path p, JImageReader reader) {
156 String entry = p.getFileName().toString().equals(MODULE_INFO)
157 ? mdir.getFileName().toString() + "/" + MODULE_INFO
158 : mdir.relativize(p).toString().replace(File.separatorChar, '/');
159
160 count.incrementAndGet();
161 String file = mdir.getFileName().toString() + "/" + entry;
162 if (APP_RESOURCES.contains(file)) {
163 // skip until the service config file is merged
164 System.out.println("Skipped " + file);
165 return;
166 }
167
168 if (reader.findLocation(entry) != null) {
169 reader.compare(entry, p);
170 }
171 }
172
173 private void loadClasses() {
174 ClassLoader loader = ClassLoader.getSystemClassLoader();
175 Stream.of(reader.getEntryNames())
176 .filter(this::accept)
177 .map(this::toClassName)
178 .forEach(cn -> {
179 count.incrementAndGet();
180 try {
181 System.out.println("Loading " + cn);
182 Class.forName(cn, false, loader);
183 } catch (VerifyError ve) {
184 System.err.println("VerifyError for " + cn);
185 failed.add(reader.imageName() + ": " + cn + " not verified: " + ve.getMessage());
186 } catch (ClassNotFoundException e) {
187 failed.add(reader.imageName() + ": " + cn + " not found");
188 }
189 });
190 }
191
192 private String toClassName(String entry) {
193 int index = entry.indexOf('/', 1);
194 return entry.substring(index + 1, entry.length())
195 .replaceAll("\\.class$", "").replace('/', '.');
196 }
197
198 // All JVMCI packages other than jdk.vm.ci.services are dynamically
199 // exported to jdk.graal.compiler
200 private static Set<String> EXCLUDED_MODULES = Set.of("jdk.graal.compiler");
201
202 private boolean accept(String entry) {
203 int index = entry.indexOf('/', 1);
204 String mn = index > 1 ? entry.substring(1, index) : "";
205 if (mn.isEmpty() || EXCLUDED_MODULES.contains(mn)) {
206 return false;
207 }
208 return entry.endsWith(".class") && !entry.endsWith(MODULE_INFO);
209 }
210
211 private static JImageReader newJImageReader() throws IOException {
212 String home = System.getProperty("java.home");
213 Path jimage = Paths.get(home, "lib", "modules");
214 System.out.println("opened " + jimage);
215 return new JImageReader(jimage);
216 }
217
218 static class JImageReader extends BasicImageReader {
219 final Path jimage;
220 JImageReader(Path p) throws IOException {
221 super(p);
222 this.jimage = p;
223 }
224
225 String imageName() {
226 return jimage.getFileName().toString();
227 }
228
229 int entries() {
230 return getHeader().getTableLength();
231 }
232
233 void compare(String entry, Path p) {
234 try {
235 byte[] bytes = Files.readAllBytes(p);
236 byte[] imagebytes = getResource(entry);
237 if (!Arrays.equals(bytes, imagebytes)) {
238 failed.add(imageName() + ": bytes differs than " + p.toString());
239 }
240 } catch (IOException e) {
241 throw new UncheckedIOException(e);
242 }
243 }
244 }
245 }
|
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 }
|