diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ResourcePoolManager.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ResourcePoolManager.java index 028bca2ecac..ba04b9db014 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ResourcePoolManager.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ResourcePoolManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -35,7 +35,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import jdk.internal.jimage.decompressor.CompressedResourceHeader; -import jdk.internal.module.Resources; +import jdk.internal.module.Checks; import jdk.internal.module.ModuleInfo; import jdk.internal.module.ModuleInfo.Attributes; import jdk.internal.module.ModuleTarget; @@ -67,11 +67,16 @@ public class ResourcePoolManager { } /** - * Returns true if a resource has an effective package. + * Returns true if a resource is located in a named package. */ - public static boolean isNamedPackageResource(String path) { - return (path.endsWith(".class") && !path.endsWith("module-info.class")) || - Resources.canEncapsulate(path); + public static boolean isNamedPackageResource(String name) { + int index = name.lastIndexOf("/"); + if (index == -1) { + return false; + } else { + String pn = name.substring(0, index).replace('/', '.'); + return Checks.isPackageName(pn); + } } static class ResourcePoolModuleImpl implements ResourcePoolModule { diff --git a/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java b/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java index ed1620ec0f0..5345ee4253c 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jmod/JmodTask.java @@ -87,7 +87,7 @@ import jdk.internal.module.ModuleInfoExtender; import jdk.internal.module.ModulePath; import jdk.internal.module.ModuleResolution; import jdk.internal.module.ModuleTarget; -import jdk.internal.module.Resources; +import jdk.internal.module.Checks; import jdk.tools.jlink.internal.Utils; import static java.util.stream.Collectors.joining; @@ -689,7 +689,6 @@ public class JmodTask { (path, attrs) -> attrs.isRegularFile(), FileVisitOption.FOLLOW_LINKS)) { return stream.map(dir::relativize) - .filter(path -> isResource(path.toString())) .map(path -> toPackageName(path)) .filter(pkg -> pkg.length() > 0) .collect(Collectors.toSet()); @@ -703,46 +702,58 @@ public class JmodTask { */ Set findPackages(JarFile jf) { return jf.stream() - .filter(e -> !e.isDirectory() && isResource(e.getName())) + .filter(e -> !e.isDirectory()) .map(e -> toPackageName(e)) .filter(pkg -> pkg.length() > 0) .collect(Collectors.toSet()); } /** - * Returns true if it's a .class or a resource with an effective - * package name. + * Maps the given relative file path to a package name. + * @throws UncheckedIOException for a class file in a top-level directory */ - boolean isResource(String name) { - name = name.replace(File.separatorChar, '/'); - return name.endsWith(".class") || Resources.canEncapsulate(name); - } + private String toPackageName(Path path) { + assert path.getRoot() == null; - - String toPackageName(Path path) { - String name = path.toString(); - int index = name.lastIndexOf(File.separatorChar); - if (index != -1) - return name.substring(0, index).replace(File.separatorChar, '.'); - - if (name.endsWith(".class") && !name.equals(MODULE_INFO)) { - IOException e = new IOException(name + " in the unnamed package"); - throw new UncheckedIOException(e); + Path parent = path.getParent(); + if (parent != null) { + String sep = path.getFileSystem().getSeparator(); + String pn = parent.toString().replace(sep, "."); + return Checks.isPackageName(pn) ? pn : ""; + } else { + // file in top-level directory + ensureNotClassFile(path.toString()); + return ""; } - return ""; } - String toPackageName(ZipEntry entry) { + /** + * Maps the name of a JAR file entry to a package name. + * @throws UncheckedIOException for a class file in a top-level directory + */ + private String toPackageName(ZipEntry entry) { String name = entry.getName(); - int index = name.lastIndexOf("/"); - if (index != -1) - return name.substring(0, index).replace('/', '.'); + assert !name.endsWith("/"); + int index = name.lastIndexOf("/"); + if (index != -1) { + String pn = name.substring(0, index).replace('/', '.'); + return Checks.isPackageName(pn) ? pn : ""; + } else { + // entry in top-level directory + ensureNotClassFile(name); + return ""; + } + } + + /** + * Throws IOException for a .class file that is not module-info.class. + */ + private void ensureNotClassFile(String name) { if (name.endsWith(".class") && !name.equals(MODULE_INFO)) { IOException e = new IOException(name + " in the unnamed package"); throw new UncheckedIOException(e); } - return ""; } void processClasses(JmodOutputStream out, List classpaths) diff --git a/test/jdk/tools/jlink/ClassFileInMetaInfo.java b/test/jdk/tools/jlink/ClassFileInMetaInfo.java new file mode 100644 index 00000000000..028da2cbe50 --- /dev/null +++ b/test/jdk/tools/jlink/ClassFileInMetaInfo.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8353267 + * @summary Test jlink with a module containing a class file in its META-INF directory + * @library /test/lib + * @modules java.base/jdk.internal.module + * jdk.jlink + * jdk.jartool + * @run junit ClassFileInMetaInfo + */ + +import java.lang.module.ModuleDescriptor; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.spi.ToolProvider; + +import jdk.test.lib.process.ProcessTools; +import jdk.test.lib.util.ModuleInfoWriter; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeAll; +import static org.junit.jupiter.api.Assertions.*; + +class ClassFileInMetaInfo { + private static PrintStream out; + private static String moduleName; + private static String classesDir; + + @BeforeAll + static void setup() throws Exception { + out = System.err; // inline with Junit + + // Create module foo containing + // module-info.class + // p/C.class + // META-INF/extra/q/C.class + moduleName = "foo"; + ModuleDescriptor descriptor = ModuleDescriptor.newModule(moduleName).build(); + byte[] moduleInfo = ModuleInfoWriter.toBytes(descriptor); + Path dir = Files.createTempDirectory(Path.of("."), moduleName); + Files.write(dir.resolve("module-info.class"), moduleInfo); + Files.createFile(Files.createDirectory(dir.resolve("p")).resolve("C.class")); + Path extraClasses = dir.resolve("META-INF/extra/"); + Files.createFile(Files.createDirectories(extraClasses.resolve("q")).resolve("C.class")); + classesDir = dir.toString(); + } + + @Test + void testExplodedModule() throws Exception { + test(classesDir); + } + + @Test + void testModularJar() throws Exception { + String jarFile = "foo.jar"; + ToolProvider jarTool = ToolProvider.findFirst("jar").orElseThrow(); + int res = jarTool.run(out, out, "cf", jarFile, "-C", classesDir, "."); + assertEquals(0, res); + test(jarFile); + } + + @Test + void testJmod() throws Exception { + String jmodFile = "foo.jmod"; + ToolProvider jmodTool = ToolProvider.findFirst("jmod").orElseThrow(); + int res = jmodTool.run(out, out, "create", "--class-path", classesDir, jmodFile); + assertEquals(0, res); + test(jmodFile); + } + + /** + * jlink --module-path .. --add-modules foo --ouptut image + * image/bin/java --describe-module foo + */ + private void test(String modulePath) throws Exception { + Path dir = Files.createTempDirectory(Path.of("."), "image"); + Files.delete(dir); + String image = dir.toString(); + + ToolProvider jlinkTool = ToolProvider.findFirst("jlink").orElseThrow(); + int res = jlinkTool.run(out, out, + "--module-path", modulePath, + "--add-modules", moduleName, + "--output", image); + assertEquals(0, res); + + var pb = new ProcessBuilder(image + "/bin/java", "--describe-module", moduleName); + ProcessTools.executeProcess(pb) + .outputTo(out) + .errorTo(out) + .shouldHaveExitValue(0) + .shouldContain(moduleName) + .shouldContain("contains p") + .shouldNotContain("META-INF"); + } +} \ No newline at end of file diff --git a/test/jdk/tools/jmod/JmodTest.java b/test/jdk/tools/jmod/JmodTest.java index d75b31cef23..9cf81e6dcaf 100644 --- a/test/jdk/tools/jmod/JmodTest.java +++ b/test/jdk/tools/jmod/JmodTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,11 +23,12 @@ /* * @test - * @bug 8142968 8166568 8166286 8170618 8168149 8240910 8276764 8276766 + * @bug 8142968 8166568 8166286 8170618 8168149 8240910 8276764 8276766 8353267 * @summary Basic test for jmod * @library /test/lib * @modules jdk.compiler * jdk.jlink + * java.base/jdk.internal.module * @build jdk.test.lib.compiler.CompilerUtils * jdk.test.lib.util.FileUtils * jdk.test.lib.Platform @@ -45,6 +46,7 @@ import java.util.spi.ToolProvider; import java.util.stream.Stream; import jdk.test.lib.compiler.CompilerUtils; import jdk.test.lib.util.FileUtils; +import jdk.test.lib.util.ModuleInfoWriter; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; @@ -682,7 +684,43 @@ public class JmodTest { Set pkgs = getModuleDescriptor(jmod).packages(); assertEquals(pkgs, expectedPackages); }); - } + } + + /** + * Test class files is the META-INF directory. + */ + @Test + public void testClassInMetaInf() throws IOException { + Path jmod = MODS_DIR.resolve("baz.jmod"); + FileUtils.deleteFileIfExistsWithRetry(jmod); + + ModuleDescriptor descriptor = ModuleDescriptor.newModule("baz").build(); + byte[] moduleInfo = ModuleInfoWriter.toBytes(descriptor); + + Path dir = Files.createTempDirectory(Path.of("."), "baz"); + Files.write(dir.resolve("module-info.class"), moduleInfo); + Files.createFile(Files.createDirectory(dir.resolve("p")).resolve("C.class")); + + // META-INF/extra/q/C.class + Path extraClasses = dir.resolve("META-INF/extra/"); + Files.createFile(Files.createDirectories(extraClasses.resolve("q")).resolve("C.class")); + + Set expectedPackages = Set.of("p"); + Set expectedContent = Set.of( + CLASSES_PREFIX + "module-info.class", + CLASSES_PREFIX + "p/C.class", + CLASSES_PREFIX + "META-INF/extra/q/C.class"); + + jmod("create", + "--class-path", dir.toString(), + jmod.toString()) + .assertSuccess() + .resultChecker(r -> { + Set pkgs = getModuleDescriptor(jmod).packages(); + assertEquals(pkgs, expectedPackages); + assertJmodContent(jmod, expectedContent); + }); + } @Test public void testVersion() {