1/*
2 * Copyright (c) 2014, 2017, 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
24import java.io.File;
25import java.io.IOException;
26import java.io.UncheckedIOException;
27import java.nio.file.DirectoryStream;
28import java.nio.file.Files;
29import java.nio.file.Path;
30import java.nio.file.Paths;
31import java.nio.file.attribute.BasicFileAttributes;
32import java.util.ArrayList;
33import java.util.Arrays;
34import java.util.Deque;
35import java.util.List;
36import java.util.Set;
37import java.util.concurrent.ConcurrentLinkedDeque;
38import java.util.concurrent.ExecutorService;
39import java.util.concurrent.Executors;
40import java.util.concurrent.TimeUnit;
41import java.util.concurrent.atomic.AtomicInteger;
42import java.util.stream.Collectors;
43import java.util.stream.Stream;
44
45import jdk.internal.jimage.BasicImageReader;
46import 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 */
64public 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    private static Set<String> EXCLUDED_MODULES =
199        Set.of("javafx.deploy", "jdk.deploy", "jdk.plugin", "jdk.javaws",
200            // All JVMCI packages other than jdk.vm.ci.services are dynamically
201            // exported to jdk.internal.vm.compiler and jdk.aot
202            "jdk.internal.vm.compiler", "jdk.aot"
203        );
204
205    private boolean accept(String entry) {
206        int index = entry.indexOf('/', 1);
207        String mn = index > 1 ? entry.substring(1, index) : "";
208        // filter deployment modules
209
210        if (mn.isEmpty() || EXCLUDED_MODULES.contains(mn)) {
211            return false;
212        }
213        return entry.endsWith(".class") && !entry.endsWith(MODULE_INFO);
214    }
215
216    private static JImageReader newJImageReader() throws IOException {
217        String home = System.getProperty("java.home");
218        Path jimage = Paths.get(home, "lib", "modules");
219        System.out.println("opened " + jimage);
220        return new JImageReader(jimage);
221    }
222
223    static class JImageReader extends BasicImageReader {
224        final Path jimage;
225        JImageReader(Path p) throws IOException {
226            super(p);
227            this.jimage = p;
228        }
229
230        String imageName() {
231            return jimage.getFileName().toString();
232        }
233
234        int entries() {
235            return getHeader().getTableLength();
236        }
237
238        void compare(String entry, Path p) {
239            try {
240                byte[] bytes = Files.readAllBytes(p);
241                byte[] imagebytes = getResource(entry);
242                if (!Arrays.equals(bytes, imagebytes)) {
243                    failed.add(imageName() + ": bytes differs than " + p.toString());
244                }
245            } catch (IOException e) {
246                throw new UncheckedIOException(e);
247            }
248        }
249    }
250}
251