1/**
2 * Copyright (c) 2015, 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
24/*
25 * @test
26 * @bug 8160286
27 * @summary Test the recording and checking of module hashes
28 * @library /test/lib
29 * @modules java.base/jdk.internal.misc
30 *          java.base/jdk.internal.module
31 *          jdk.compiler
32 *          jdk.jartool
33 *          jdk.jlink
34 * @build jdk.test.lib.compiler.ModuleInfoMaker
35 *        jdk.test.lib.compiler.CompilerUtils
36 * @run testng HashesTest
37 */
38
39import java.io.File;
40import java.io.IOException;
41import java.io.InputStream;
42import java.io.UncheckedIOException;
43import java.lang.module.ModuleDescriptor;
44import java.lang.module.ModuleFinder;
45import java.lang.module.ModuleReader;
46import java.lang.module.ModuleReference;
47import java.nio.file.FileVisitResult;
48import java.nio.file.Files;
49import java.nio.file.Path;
50import java.nio.file.Paths;
51import java.nio.file.SimpleFileVisitor;
52import java.nio.file.attribute.BasicFileAttributes;
53import java.util.ArrayList;
54import java.util.Arrays;
55import java.util.Collections;
56import java.util.List;
57import java.util.Set;
58import java.util.spi.ToolProvider;
59import java.util.stream.Collectors;
60import java.util.stream.Stream;
61
62import jdk.internal.module.ModuleInfo;
63import jdk.internal.module.ModuleHashes;
64import jdk.internal.module.ModulePath;
65
66import jdk.test.lib.compiler.ModuleInfoMaker;
67
68import org.testng.annotations.Test;
69
70import static org.testng.Assert.*;
71import static java.lang.module.ModuleDescriptor.Requires.Modifier.*;
72
73public class HashesTest {
74    static final ToolProvider JMOD_TOOL = ToolProvider.findFirst("jmod")
75        .orElseThrow(() ->
76            new RuntimeException("jmod tool not found")
77        );
78    static final ToolProvider JAR_TOOL = ToolProvider.findFirst("jar")
79        .orElseThrow(() ->
80            new RuntimeException("jar tool not found")
81        );
82
83    private final Path mods;
84    private final Path srcDir;
85    private final Path lib;
86    private final ModuleInfoMaker builder;
87    HashesTest(Path dest) throws IOException {
88        if (Files.exists(dest)) {
89            deleteDirectory(dest);
90        }
91        this.mods = dest.resolve("mods");
92        this.srcDir = dest.resolve("src");
93        this.lib = dest.resolve("lib");
94        this.builder = new ModuleInfoMaker(srcDir);
95
96        Files.createDirectories(lib);
97        Files.createDirectories(mods);
98    }
99
100    @Test
101    public static void test() throws IOException {
102        Path dest = Paths.get("test");
103        HashesTest ht = new HashesTest(dest);
104
105        // create modules for test cases
106        ht.makeModule("m2");
107        ht.makeModule("m3");
108        ht.makeModule("m1", "m2", "m3");
109
110        ht.makeModule("org.bar", TRANSITIVE, "m1");
111        ht.makeModule("org.foo", TRANSITIVE, "org.bar");
112
113        // create JMOD for m1, m2, m3
114        ht.makeJmod("m2");
115        ht.makeJmod("m3");
116
117        // no hash is recorded since m1 has outgoing edges
118        ht.jmodHashModules("m1", ".*");
119
120        // no hash is recorded in m1, m2, m3
121        assertTrue(ht.hashes("m1") == null);
122        assertTrue(ht.hashes("m2") == null);
123        assertTrue(ht.hashes("m3") == null);
124
125        // hash m1 in m2
126        ht.jmodHashModules("m2",  "m1");
127        ht.checkHashes("m2", "m1");
128
129        // hash m1 in m2
130        ht.jmodHashModules("m2",  ".*");
131        ht.checkHashes("m2", "m1");
132
133        // create m2.jmod with no hash
134        ht.makeJmod("m2");
135        // run jmod hash command to hash m1 in m2 and m3
136        runJmod(List.of("hash", "--module-path", ht.lib.toString(),
137                        "--hash-modules", ".*"));
138        ht.checkHashes("m2", "m1");
139        ht.checkHashes("m3", "m1");
140
141        // check transitive requires
142        ht.makeJmod("org.bar");
143        ht.makeJmod("org.foo");
144
145        ht.jmodHashModules("org.bar", "org.*");
146        ht.checkHashes("org.bar", "org.foo");
147
148        ht.jmodHashModules( "m3", ".*");
149        ht.checkHashes("m3", "org.foo", "org.bar", "m1");
150    }
151
152    @Test
153    public static void multiBaseModules() throws IOException {
154        Path dest = Paths.get("test2");
155        HashesTest ht = new HashesTest(dest);
156
157        /*
158         * y2 -----------> y1
159         *    |______
160         *    |      |
161         *    V      V
162         *    z3 -> z2
163         *    |      |
164         *    |      V
165         *    |---> z1
166         */
167
168        ht.makeModule("z1");
169        ht.makeModule("z2", "z1");
170        ht.makeModule("z3", "z1", "z2");
171
172        ht.makeModule("y1");
173        ht.makeModule("y2", "y1", "z2", "z3");
174
175        Set<String> ys = Set.of("y1", "y2");
176        Set<String> zs = Set.of("z1", "z2", "z3");
177
178        // create JMOD files
179        Stream.concat(ys.stream(), zs.stream()).forEach(ht::makeJmod);
180
181        // run jmod hash command
182        runJmod(List.of("hash", "--module-path", ht.lib.toString(),
183                        "--hash-modules", ".*"));
184
185        /*
186         * z1 and y1 are the modules with hashes recorded.
187         */
188        ht.checkHashes("y1", "y2");
189        ht.checkHashes("z1", "z2", "z3", "y2");
190        Stream.concat(ys.stream(), zs.stream())
191              .filter(mn -> !mn.equals("y1") && !mn.equals("z1"))
192              .forEach(mn -> assertTrue(ht.hashes(mn) == null));
193    }
194
195    @Test
196    public static void mixJmodAndJarFile() throws IOException {
197        Path dest = Paths.get("test3");
198        HashesTest ht = new HashesTest(dest);
199
200        /*
201         * j3 -----------> j2
202         *    |______
203         *    |      |
204         *    V      V
205         *    m3 -> m2
206         *    |      |
207         *    |      V
208         *    |---> m1 -> j1 -> jdk.jlink
209         */
210
211        ht.makeModule("j1");
212        ht.makeModule("j2");
213        ht.makeModule("m1", "j1");
214        ht.makeModule("m2", "m1");
215        ht.makeModule("m3", "m1", "m2");
216
217        ht.makeModule("j3", "j2", "m2", "m3");
218
219        Set<String> jars = Set.of("j1", "j2", "j3");
220        Set<String> jmods = Set.of("m1", "m2", "m3");
221
222        // create JMOD and JAR files
223        jars.forEach(ht::makeJar);
224        jmods.forEach(ht::makeJmod);
225
226        // run jmod hash command
227        runJmod(List.of("hash", "--module-path", ht.lib.toString(),
228                        "--hash-modules", "^j.*|^m.*"));
229
230        /*
231         * j1 and j2 are the modules with hashes recorded.
232         */
233        ht.checkHashes("j2", "j3");
234        ht.checkHashes("j1", "m1", "m2", "m3", "j3");
235        Stream.concat(jars.stream(), jmods.stream())
236              .filter(mn -> !mn.equals("j1") && !mn.equals("j2"))
237              .forEach(mn -> assertTrue(ht.hashes(mn) == null));
238    }
239
240    @Test
241    public static void upgradeableModule() throws IOException {
242        Path mpath = Paths.get(System.getProperty("java.home"), "jmods");
243        if (!Files.exists(mpath)) {
244            return;
245        }
246
247        Path dest = Paths.get("test4");
248        HashesTest ht = new HashesTest(dest);
249        ht.makeModule("m1");
250        ht.makeModule("java.xml.bind", "m1");
251        ht.makeModule("java.xml.ws", "java.xml.bind");
252        ht.makeModule("m2", "java.xml.ws");
253
254        ht.makeJmod("m1");
255        ht.makeJmod("m2");
256        ht.makeJmod("java.xml.ws");
257        ht.makeJmod("java.xml.bind",
258                    "--module-path",
259                    ht.lib.toString() + File.pathSeparator + mpath,
260                    "--hash-modules", "^java.xml.*|^m.*");
261
262        ht.checkHashes("java.xml.bind", "java.xml.ws", "m2");
263    }
264
265    @Test
266    public static void testImageJmods() throws IOException {
267        Path mpath = Paths.get(System.getProperty("java.home"), "jmods");
268        if (!Files.exists(mpath)) {
269            return;
270        }
271
272        Path dest = Paths.get("test5");
273        HashesTest ht = new HashesTest(dest);
274        ht.makeModule("m1", "jdk.compiler", "jdk.attach");
275        ht.makeModule("m2", "m1");
276        ht.makeModule("m3", "java.compiler");
277
278        ht.makeJmod("m1");
279        ht.makeJmod("m2");
280
281        runJmod(List.of("hash",
282                        "--module-path",
283                        mpath.toString() + File.pathSeparator + ht.lib.toString(),
284                        "--hash-modules", ".*"));
285
286        validateImageJmodsTest(ht, mpath);
287    }
288
289    @Test
290    public static void testImageJmods1() throws IOException {
291        Path mpath = Paths.get(System.getProperty("java.home"), "jmods");
292        if (!Files.exists(mpath)) {
293            return;
294        }
295
296        Path dest = Paths.get("test6");
297        HashesTest ht = new HashesTest(dest);
298        ht.makeModule("m1", "jdk.compiler", "jdk.attach");
299        ht.makeModule("m2", "m1");
300        ht.makeModule("m3", "java.compiler");
301
302        ht.makeJar("m2");
303        ht.makeJar("m1",
304                    "--module-path",
305                    mpath.toString() + File.pathSeparator + ht.lib.toString(),
306                    "--hash-modules", ".*");
307        validateImageJmodsTest(ht, mpath);
308    }
309
310    private static void validateImageJmodsTest(HashesTest ht, Path mpath)
311        throws IOException
312    {
313        // hash is recorded in m1 and not any other packaged modules on module path
314        ht.checkHashes("m1", "m2");
315        assertTrue(ht.hashes("m2") == null);
316
317        // should not override any JDK packaged modules
318        ModuleFinder finder = ModulePath.of(Runtime.version(), true, mpath);
319        assertTrue(ht.hashes(finder,"jdk.compiler") == null);
320        assertTrue(ht.hashes(finder,"jdk.attach") == null);
321    }
322
323    private void checkHashes(String mn, String... hashModules) throws IOException {
324        ModuleHashes hashes = hashes(mn);
325        assertTrue(hashes.names().equals(Set.of(hashModules)));
326    }
327
328    private ModuleHashes hashes(String name) {
329        ModuleFinder finder = ModulePath.of(Runtime.version(), true, lib);
330        return hashes(finder, name);
331    }
332
333    private ModuleHashes hashes(ModuleFinder finder, String name) {
334        ModuleReference mref = finder.find(name).orElseThrow(RuntimeException::new);
335        try {
336            ModuleReader reader = mref.open();
337            try (InputStream in = reader.open("module-info.class").get()) {
338                ModuleHashes hashes = ModuleInfo.read(in, null).recordedHashes();
339                System.out.format("hashes in module %s %s%n", name,
340                    (hashes != null) ? "present" : "absent");
341                if (hashes != null) {
342                    hashes.names().stream().sorted().forEach(n ->
343                        System.out.format("  %s %s%n", n, toHex(hashes.hashFor(n)))
344                    );
345                }
346                return hashes;
347            } finally {
348                reader.close();
349            }
350        } catch (IOException e) {
351            throw new UncheckedIOException(e);
352        }
353    }
354
355    private String toHex(byte[] ba) {
356        StringBuilder sb = new StringBuilder(ba.length);
357        for (byte b: ba) {
358            sb.append(String.format("%02x", b & 0xff));
359        }
360        return sb.toString();
361    }
362
363    private void deleteDirectory(Path dir) throws IOException {
364        Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
365            @Override
366            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
367                throws IOException
368            {
369                Files.delete(file);
370                return FileVisitResult.CONTINUE;
371            }
372
373            @Override
374            public FileVisitResult postVisitDirectory(Path dir, IOException exc)
375                throws IOException
376            {
377                Files.delete(dir);
378                return FileVisitResult.CONTINUE;
379            }
380        });
381    }
382
383
384    private void makeModule(String mn, String... deps) throws IOException {
385        makeModule(mn, null, deps);
386    }
387
388    private void makeModule(String mn, ModuleDescriptor.Requires.Modifier mod, String... deps)
389        throws IOException
390    {
391        if (mod != null && mod != TRANSITIVE && mod != STATIC) {
392            throw new IllegalArgumentException(mod.toString());
393        }
394
395        StringBuilder sb = new StringBuilder();
396        sb.append("module ")
397          .append(mn)
398          .append(" {")
399          .append("\n");
400        Arrays.stream(deps)
401              .forEach(req -> {
402                  sb.append("    requires ");
403                  if (mod != null) {
404                      sb.append(mod.toString().toLowerCase())
405                        .append(" ");
406                  }
407                  sb.append(req)
408                    .append(";\n");
409              });
410        sb.append("}\n");
411        builder.writeJavaFiles(mn, sb.toString());
412        builder.compile(mn, mods);
413    }
414
415    private void jmodHashModules(String moduleName, String hashModulesPattern) {
416        makeJmod(moduleName, "--module-path", lib.toString(),
417                 "--hash-modules", hashModulesPattern);
418    }
419
420    private void makeJmod(String moduleName, String... options) {
421        Path mclasses = mods.resolve(moduleName);
422        Path outfile = lib.resolve(moduleName + ".jmod");
423        List<String> args = new ArrayList<>();
424        args.add("create");
425        Collections.addAll(args, options);
426        Collections.addAll(args, "--class-path", mclasses.toString(),
427                           outfile.toString());
428
429        if (Files.exists(outfile)) {
430            try {
431                Files.delete(outfile);
432            } catch (IOException e) {
433                throw new UncheckedIOException(e);
434            }
435        }
436        runJmod(args);
437    }
438
439    private static void runJmod(List<String> args) {
440        int rc = JMOD_TOOL.run(System.out, System.out, args.toArray(new String[args.size()]));
441        System.out.println("jmod " + args.stream().collect(Collectors.joining(" ")));
442        if (rc != 0) {
443            throw new AssertionError("jmod failed: rc = " + rc);
444        }
445    }
446
447    private void makeJar(String moduleName, String... options) {
448        Path mclasses = mods.resolve(moduleName);
449        Path outfile = lib.resolve(moduleName + ".jar");
450        List<String> args = new ArrayList<>();
451        Stream.concat(Stream.of("--create",
452                                "--file=" + outfile.toString()),
453                      Arrays.stream(options))
454              .forEach(args::add);
455        args.add("-C");
456        args.add(mclasses.toString());
457        args.add(".");
458
459        if (Files.exists(outfile)) {
460            try {
461                Files.delete(outfile);
462            } catch (IOException e) {
463                throw new UncheckedIOException(e);
464            }
465        }
466
467        int rc = JAR_TOOL.run(System.out, System.out, args.toArray(new String[args.size()]));
468        System.out.println("jar " + args.stream().collect(Collectors.joining(" ")));
469        if (rc != 0) {
470            throw new AssertionError("jar failed: rc = " + rc);
471        }
472    }
473}
474