1 /*
  2  * Copyright (c) 2015, 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 /*
 25  * @test
 26  * @bug 8142968 8300228
 27  * @library /test/lib
 28  * @modules java.base/jdk.internal.module
 29  *          jdk.compiler
 30  *          jdk.jlink
 31  * @build ModuleReaderTest
 32  *        jdk.test.lib.compiler.CompilerUtils
 33  *        jdk.test.lib.util.JarUtils
 34  * @run junit ModuleReaderTest
 35  * @summary Basic tests for java.lang.module.ModuleReader
 36  */
 37 
 38 import java.io.File;
 39 import java.io.IOException;
 40 import java.io.InputStream;
 41 import java.lang.module.ModuleFinder;
 42 import java.lang.module.ModuleReader;
 43 import java.lang.module.ModuleReference;
 44 import java.net.URI;
 45 import java.net.URL;
 46 import java.net.URLConnection;
 47 import java.nio.ByteBuffer;
 48 import java.nio.charset.StandardCharsets;
 49 import java.nio.file.Files;
 50 import java.nio.file.Path;
 51 import java.nio.file.Paths;
 52 import java.util.HashSet;
 53 import java.util.List;
 54 import java.util.Optional;
 55 import java.util.Set;
 56 import java.util.spi.ToolProvider;
 57 import java.util.stream.Stream;
 58 
 59 import jdk.internal.module.ModulePath;
 60 import jdk.test.lib.Utils;
 61 import jdk.test.lib.compiler.CompilerUtils;
 62 import jdk.test.lib.util.JarUtils;
 63 import org.junit.jupiter.api.BeforeAll;
 64 import org.junit.jupiter.api.Test;
 65 
 66 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 67 import static org.junit.jupiter.api.Assertions.assertEquals;
 68 import static org.junit.jupiter.api.Assertions.assertFalse;
 69 import static org.junit.jupiter.api.Assertions.assertThrows;
 70 import static org.junit.jupiter.api.Assertions.assertTrue;
 71 
 72 public class ModuleReaderTest {
 73     private static final Path MODS_DIR = Paths.get("mods");
 74 
 75     // the module name of the base module
 76     private static final String BASE_MODULE = "java.base";
 77 
 78     // the module name of the test module
 79     private static final String TEST_MODULE = "m";
 80 
 81     // resources in the base module
 82     private static final String[] BASE_RESOURCES = {
 83         "java/lang/Object.class"
 84     };
 85 
 86     // (directory) resources that may be in the base module
 87     private static final String[] MAYBE_BASE_RESOURCES = {
 88         "java",
 89         "java/",
 90         "java/lang",
 91         "java/lang/",
 92     };
 93 
 94     // resource names that should not be found in the base module
 95     private static final String[] NOT_BASE_RESOURCES = {
 96         "NotFound",
 97         "/java",
 98         "//java",
 99         "/java/lang",
100         "//java/lang",
101         "java//lang",
102         "/java/lang/Object.class",
103         "//java/lang/Object.class",
104         "java/lang/Object.class/",
105         "java//lang//Object.class",
106         "./java/lang/Object.class",
107         "java/./lang/Object.class",
108         "java/lang/./Object.class",
109         "../java/lang/Object.class",
110         "java/../lang/Object.class",
111         "java/lang/../Object.class",
112 
113         // junk resource names
114         "java\u0000",
115         "C:java",
116         "C:\\java",
117         "java\\lang\\Object.class"
118     };
119 
120     // Resources in test module (can't use module-info.class as a test
121     // resource as it will be modified by the jmod tool)
122     private static final String[] TEST_RESOURCES = {
123             "p/Main.class",
124             "p/test.txt"
125     };
126 
127     // (directory) resources that may be in the test module
128     private static final String[] MAYBE_TEST_RESOURCES = {
129         "p",
130         "p/"
131     };
132 
133     // resource names that should not be found in the test module
134     private static final String[] NOT_TEST_RESOURCES = {
135         "NotFound",
136         "/p",
137         "//p",
138         "/p/Main.class",
139         "//p/Main.class",
140         "p/Main.class/",
141         "p//Main.class",
142         "./p/Main.class",
143         "p/./Main.class",
144         "../p/Main.class",
145         "p/../p/Main.class",
146 
147         // junk resource names
148         "p\u0000",
149         "C:p",
150         "C:\\p",
151         "p\\Main.class"
152     };
153 
154     @BeforeAll
155     public static void compileTestModules() throws Exception {
156         // Write simplest module-info class.
157         Path srcDir = Path.of("src", TEST_MODULE);
158         Files.createDirectories(srcDir);
159         Files.writeString(srcDir.resolve("module-info.java"), "module " + TEST_MODULE + " {}");
160 
161         // Write and compile test class "p.Main".
162         Path pkgPath = Path.of("p");
163         Path javaSrc = srcDir.resolve(pkgPath).resolve("Main.java");
164         Files.createDirectories(javaSrc.getParent());
165         Files.writeString(javaSrc,
166                 """
167                 package p;
168                 public class Main {
169                     public static void main(String[] args) { }
170                 }
171                 """);
172 
173         // javac -d <outDir> <srcDir>/**
174         Path outDir = MODS_DIR.resolve(TEST_MODULE);
175         boolean compiled = CompilerUtils.compile(srcDir, outDir);
176         assertTrue(compiled, "test module did not compile");
177 
178         // Add two versions of a resource for preview mode testing.
179         Files.writeString(outDir.resolve(pkgPath).resolve("test.txt"), "Normal Version");
180         Path previewDir = outDir.resolve("META-INF", "preview").resolve(pkgPath);
181         Files.createDirectories(previewDir);
182         Files.writeString(previewDir.resolve("test.txt"), "Preview Version");
183     }
184 
185     /**
186      * Test ModuleReader with module in runtime image.
187      */
188     @Test
189     public void testImage() throws IOException {
190         ModuleFinder finder = ModuleFinder.ofSystem();
191         ModuleReference mref = finder.find(BASE_MODULE).get();
192         ModuleReader reader = mref.open();
193 
194         try (reader) {
195 
196             for (String name : BASE_RESOURCES) {
197                 byte[] expectedBytes;
198                 Module baseModule = Object.class.getModule();
199                 try (InputStream in = baseModule.getResourceAsStream(name)) {
200                     expectedBytes = in.readAllBytes();
201                 }
202 
203                 testFind(reader, name, expectedBytes);
204                 testOpen(reader, name, expectedBytes);
205                 testRead(reader, name, expectedBytes);
206                 testList(reader, name);
207             }
208 
209             // test resources that may be in the base module
210             for (String name : MAYBE_BASE_RESOURCES) {
211                 Optional<URI> ouri = reader.find(name);
212                 ouri.ifPresent(uri -> {
213                     if (name.endsWith("/"))
214                         assertTrue(uri.toString().endsWith("/"),
215                                 "mismatched directory URI for '" + name + "': " + uri);
216                 });
217             }
218 
219             // test "not found" in java.base module
220             for (String name : NOT_BASE_RESOURCES) {
221                 assertFalse(reader.find(name).isPresent(), "Unexpected resource found: " + name);
222                 assertFalse(reader.open(name).isPresent(), "Unexpected resource opened: " + name);
223                 assertFalse(reader.read(name).isPresent(), "Unexpected resource read: " + name);
224             }
225 
226             // test nulls
227             assertThrows(NullPointerException.class, () -> reader.find(null));
228             assertThrows(NullPointerException.class, () -> reader.open(null));
229             assertThrows(NullPointerException.class, () -> reader.read(null));
230             assertThrows(NullPointerException.class, () -> reader.release(null));
231         }
232 
233         // test closed ModuleReader
234         assertThrows(IOException.class, () -> reader.open(BASE_RESOURCES[0]));
235         assertThrows(IOException.class, () -> reader.read(BASE_RESOURCES[0]));
236         assertThrows(IOException.class, reader::list);
237     }
238 
239     /**
240      * Test ModuleReader with exploded module.
241      */
242     @Test
243     public void testExplodedModule() throws IOException {
244         test(MODS_DIR);
245     }
246 
247     /**
248      * Test equivalent of the system ModuleReader with preview mode. This differs
249      * in behavior to other "exploded modules" because it supports preview mode.
250      * It also hides preview resources when preview mode is enabled.
251      *
252      * <p>Note: When preview mode is not enabled, preview resources are visible
253      * via their un-mapped path. This is not the same behavior as things like
254      * the JRT filesystem or non-exploded module readers, in which preview paths
255      * are always hidden.
256      */
257     @Test
258     public void testExplodedSystemModule() throws IOException {
259         ModuleFinder normalFinder = ModulePath.of(/* modulePatcher */ null, /* previewMode */ false, MODS_DIR);
260         try (ModuleReader reader = normalFinder.find(TEST_MODULE).get().open()) {
261             assertEquals("Normal Version", assertUtf8Resource(reader, "p/test.txt"));
262             // This file is not visible in an exploded image when using JRT filesystem.
263             assertEquals("Preview Version", assertUtf8Resource(reader, "META-INF/preview/p/test.txt"));
264         }
265         ModuleFinder previewFinder = ModulePath.of(/* modulePatcher */ null, /* previewMode */ true, MODS_DIR);
266         try (ModuleReader reader = previewFinder.find(TEST_MODULE).get().open()) {
267             assertEquals("Preview Version", assertUtf8Resource(reader, "p/test.txt"));
268             assertFalse(reader.find("META-INF/preview/p/test.txt").isPresent(), "unexpected preview resource");
269         }
270     }
271 
272     private static String assertUtf8Resource(ModuleReader reader, String name) throws IOException {
273         // Check the resource can be found with the expected URI.
274         Optional<URI> uri = reader.find(name);
275         assertTrue(uri.isPresent(), "resource not found: " + name);
276         assertTrue(uri.get().getPath().endsWith(name), "unexpected path: " + uri.get());
277 
278         // Open and read all resource bytes.
279         Optional<InputStream> is = reader.open(name);
280         assertTrue(is.isPresent(), "resource cannot be opened: " + name);
281         byte[] bytes = is.get().readAllBytes();
282 
283         // Cross-check that read() returns the same bytes as open().
284         Optional<ByteBuffer> buffer = reader.read(name);
285         assertTrue(buffer.isPresent(), "resource cannot be read: " + name);
286         assertArrayEquals(buffer.get().array(), bytes, "resource bytes differ: " + name);
287         // Return the string of the UTF-8 bytes for checking the actual content.
288         return new String(bytes, StandardCharsets.UTF_8);
289     }
290 
291     /**
292      * Test ModuleReader with module in modular JAR.
293      */
294     @Test
295     public void testModularJar() throws IOException {
296         Path dir = Utils.createTempDirectory("mlib");
297 
298         // jar cf mlib/${TESTMODULE}.jar -C mods .
299         JarUtils.createJarFile(dir.resolve(TEST_MODULE + ".jar"),
300                 MODS_DIR.resolve(TEST_MODULE));
301 
302         test(dir);
303     }
304 
305     /**
306      * Test ModuleReader with module in a JMOD file.
307      */
308     @Test
309     public void testJMod() throws IOException {
310         Path dir = Utils.createTempDirectory("mlib");
311 
312         // jmod create --class-path mods/${TESTMODULE}  mlib/${TESTMODULE}.jmod
313         String cp = MODS_DIR.resolve(TEST_MODULE).toString();
314         String jmod = dir.resolve(TEST_MODULE + ".jmod").toString();
315         String[] args = {"create", "--class-path", cp, jmod};
316         ToolProvider jmodTool = ToolProvider.findFirst("jmod")
317                 .orElseThrow(() ->
318                         new RuntimeException("jmod tool not found")
319                 );
320         assertEquals(0, jmodTool.run(System.out, System.out, args), "jmod tool failed");
321 
322         test(dir);
323     }
324 
325     /**
326      * The test module is found on the given module path. Open a ModuleReader
327      * to the test module and test the reader.
328      */
329     void test(Path mp) throws IOException {
330         ModuleFinder finder = ModulePath.of(Runtime.version(), true, mp);
331         ModuleReference mref = finder.find(TEST_MODULE).get();
332         ModuleReader reader = mref.open();
333 
334         try (reader) {
335 
336             // test resources in test module
337             for (String name : TEST_RESOURCES) {
338                 System.out.println("resource: " + name);
339                 byte[] expectedBytes
340                     = Files.readAllBytes(MODS_DIR
341                         .resolve(TEST_MODULE)
342                         .resolve(name.replace('/', File.separatorChar)));
343 
344                 testFind(reader, name, expectedBytes);
345                 testOpen(reader, name, expectedBytes);
346                 testRead(reader, name, expectedBytes);
347                 testList(reader, name);
348             }
349 
350             // test resources that may be in the test module
351             for (String name : MAYBE_TEST_RESOURCES) {
352                 System.out.println("resource: " + name);
353                 Optional<URI> ouri = reader.find(name);
354                 ouri.ifPresent(uri -> {
355                     if (name.endsWith("/"))
356                         assertTrue(uri.toString().endsWith("/"),
357                                 "mismatched directory URI for '" + name + "': " + uri);
358                 });
359             }
360 
361             // test "not found" in test module
362             for (String name : NOT_TEST_RESOURCES) {
363                 System.out.println("resource: " + name);
364                 assertFalse(reader.find(name).isPresent(), "Unexpected resource found: " + name);
365                 assertFalse(reader.open(name).isPresent(), "Unexpected resource open: " + name);
366                 assertFalse(reader.read(name).isPresent(), "Unexpected resource read: " + name);
367             }
368 
369             // test nulls
370             assertThrows(NullPointerException.class, () -> reader.find(null));
371             assertThrows(NullPointerException.class, () -> reader.open(null));
372             assertThrows(NullPointerException.class, () -> reader.read(null));
373             assertThrows(NullPointerException.class, () -> reader.release(null));
374         }
375 
376         // test closed ModuleReader
377         assertThrows(IOException.class, () -> reader.open(BASE_RESOURCES[0]));
378         assertThrows(IOException.class, () -> reader.read(BASE_RESOURCES[0]));
379         assertThrows(IOException.class, reader::list);
380     }
381 
382     /**
383      * Test ModuleReader#find
384      */
385     void testFind(ModuleReader reader, String name, byte[] expectedBytes)
386         throws IOException
387     {
388         Optional<URI> ouri = reader.find(name);
389         assertTrue(ouri.isPresent(), "missing URI for: " + name);
390 
391         URL url = ouri.get().toURL();
392         if (!url.getProtocol().equalsIgnoreCase("jmod")) {
393             URLConnection uc = url.openConnection();
394             uc.setUseCaches(false);
395             try (InputStream in = uc.getInputStream()) {
396                 byte[] bytes = in.readAllBytes();
397                 assertArrayEquals(expectedBytes, bytes, "resource bytes differ for: " + name);
398             }
399         }
400     }
401 
402     /**
403      * Test ModuleReader#open
404      */
405     void testOpen(ModuleReader reader, String name, byte[] expectedBytes)
406         throws IOException
407     {
408         Optional<InputStream> oin = reader.open(name);
409         assertTrue(oin.isPresent(), "missing input stream for: " + name);
410         try (InputStream in = oin.get()) {
411             byte[] bytes = in.readAllBytes();
412             assertArrayEquals(expectedBytes, bytes, "resource bytes differ for: " + name);
413         }
414     }
415 
416     /**
417      * Test ModuleReader#read
418      */
419     void testRead(ModuleReader reader, String name, byte[] expectedBytes)
420         throws IOException
421     {
422         Optional<ByteBuffer> obb = reader.read(name);
423         assertTrue(obb.isPresent());
424 
425         ByteBuffer bb = obb.get();
426         try {
427             int rem = bb.remaining();
428             assertEquals(expectedBytes.length, rem, "resource lengths differ: " + name);
429             byte[] bytes = new byte[rem];
430             bb.get(bytes);
431             assertArrayEquals(expectedBytes, bytes, "resource bytes differ: " + name);
432         } finally {
433             reader.release(bb);
434         }
435     }
436 
437     /**
438      * Test ModuleReader#list
439      */
440     void testList(ModuleReader reader, String name) throws IOException {
441         final List<String> list;
442         try (Stream<String> stream = reader.list()) {
443             list = stream.toList();
444         }
445         Set<String> names = new HashSet<>(list);
446         assertEquals(names.size(), list.size(), "resource list contains duplicates: " + list);
447 
448         assertTrue(names.contains("module-info.class"), "resource list did not contain 'module-info.class': " + list);
449         assertTrue(names.contains(name), "resource list did not contain '" + name + "'" + list);
450 
451         // all resources should be locatable via find
452         for (String e : names) {
453             assertTrue(reader.find(e).isPresent(), "resource not found: " + name);
454         }
455     }
456 
457 }