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 }