diff --git a/src/jdk.jartool/share/classes/sun/tools/jar/Validator.java b/src/jdk.jartool/share/classes/sun/tools/jar/Validator.java index 1852b2cad9d..be694eeb1bc 100644 --- a/src/jdk.jartool/share/classes/sun/tools/jar/Validator.java +++ b/src/jdk.jartool/share/classes/sun/tools/jar/Validator.java @@ -260,6 +260,19 @@ final class Validator { isValid = false; warn(getMsg("warn.validator.order.mismatch")); } + // Check location of an optional manifest entry + if ("META-INF/MANIFEST.MF".equals(entryName)) { + int index = entryInfo.cen().order(); + if (index > 1) { // Expect base manifest at index 0 or 1 + String position = Integer.toString(index); + errorAndInvalid(formatMsg("error.validator.manifest.wrong.position", position)); + } else if (index == 1) { // Ensure "META-INF/" preceeds manifest + String firstName = entries.sequencedKeySet().getFirst(); + if (!"META-INF/".equals(firstName)) { + errorAndInvalid(formatMsg("error.validator.metainf.wrong.position", firstName)); + } + } + } } /** diff --git a/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties b/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties index 0e3555f181e..c80377e20c1 100644 --- a/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties +++ b/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties @@ -134,6 +134,10 @@ error.validator.info.version.notequal=\ {0}: module-info.class in a versioned directory contains different "version" error.validator.info.manclass.notequal=\ {0}: module-info.class in a versioned directory contains different "main-class" +error.validator.metainf.wrong.position=\ + expected entry META-INF/ to be at position 0, but found: {0} +error.validator.manifest.wrong.position=\ + expected entry META-INF/MANIFEST.MF to be at position 0 or 1, but found it at position: {0} warn.validator.identical.entry=\ Warning: entry {0} contains a class that\n\ is identical to an entry already in the jar diff --git a/test/jdk/tools/jar/ValidatorTest.java b/test/jdk/tools/jar/ValidatorTest.java index c08e6a8a984..c2dfeae8dec 100644 --- a/test/jdk/tools/jar/ValidatorTest.java +++ b/test/jdk/tools/jar/ValidatorTest.java @@ -38,10 +38,13 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertLinesMatch; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.io.IOException; +import java.io.FileInputStream; import java.io.PrintStream; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; @@ -50,6 +53,7 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.jar.JarFile; +import java.util.jar.JarInputStream; import java.util.spi.ToolProvider; import java.util.stream.Stream; import java.util.zip.ZipEntry; @@ -306,6 +310,96 @@ class ValidatorTest { } } + /** + * Validates that base manifest-related entries are at expected LOC positions. + *

+ * Copied from JarInputStream.java: + *

+     * This implementation assumes the META-INF/MANIFEST.MF entry
+     * should be either the first or the second entry (when preceded
+     * by the dir META-INF/). It skips the META-INF/ and then
+     * "consumes" the MANIFEST.MF to initialize the Manifest object.
+     * 
+ * This test does not do a similar CEN check in the event that the LOC and CEN + * entries do not match. Those mismatch cases are already checked by other tests. + */ + @Test + public void testWrongManifestPositions() throws IOException { + testWrongManifestPosition( + Path.of("wrong-entry-position-A.jar"), + """ + expected entry META-INF/ to be at position 0, but found: PLACEHOLDER + """, + EntryWriter.ofText("PLACEHOLDER", "0"), + EntryWriter.ofText(META_INF + "MANIFEST.MF", "Manifest-Version: 1.0")); + testWrongManifestPosition( + Path.of("wrong-entry-position-B.jar"), + """ + expected entry META-INF/MANIFEST.MF to be at position 0 or 1, but found it at position: 2 + """, + EntryWriter.ofDirectory(META_INF), + EntryWriter.ofText("PLACEHOLDER", "1"), + EntryWriter.ofText(META_INF + "MANIFEST.MF", "Manifest-Version: 1.0")); + testWrongManifestPosition( + Path.of("wrong-entry-position-C.jar"), + """ + expected entry META-INF/MANIFEST.MF to be at position 0 or 1, but found it at position: 4 + """, + EntryWriter.ofDirectory(META_INF), + EntryWriter.ofText("PLACEHOLDER1", "1"), + EntryWriter.ofText("PLACEHOLDER2", "2"), + EntryWriter.ofText("PLACEHOLDER3", "3"), + EntryWriter.ofText(META_INF + "MANIFEST.MF", "Manifest-Version: 1.0")); + } + + private void testWrongManifestPosition( + Path path, String expectedErrorMessage, EntryWriter... entries) throws IOException { + createZipFile(path, entries); + // first check JAR file with streaming API + try (var jis = new JarInputStream(new FileInputStream(path.toFile()))) { + var manifest = jis.getManifest(); + assertNull(manifest, "Manifest not null?!"); + } + // now validate with tool CLI + try { + jar("--validate --file " + path); + fail("Expecting non-zero exit code validating: " + path); + } catch (IOException e) { + var err = e.getMessage(); + System.out.println(err); + assertLinesMatch(expectedErrorMessage.lines(), err.lines()); + } + } + + record EntryWriter(ZipEntry entry, Writer writer) { + @FunctionalInterface + interface Writer { + void write(ZipOutputStream stream) throws IOException; + } + static EntryWriter ofDirectory(String name) { + return new EntryWriter(new ZipEntry(name), _ -> {}); + } + static EntryWriter ofText(String name, String text) { + return new EntryWriter(new ZipEntry(name), + stream -> stream.write(text.getBytes(StandardCharsets.UTF_8))); + } + } + + private static void createZipFile(Path path, EntryWriter... entries) throws IOException { + System.out.printf("%n%n*****Creating Zip file with %d entries*****%n".formatted(entries.length)); + var out = new ByteArrayOutputStream(1024); + try (var zos = new ZipOutputStream(out)) { + for (var entry : entries) { + System.out.printf(" %s%n".formatted(entry.entry().getName())); + zos.putNextEntry(entry.entry()); + entry.writer().write(zos); + zos.closeEntry(); + } + zos.flush(); + } + Files.write(path, out.toByteArray()); + } + // return stderr output private String jar(String cmdline) throws IOException { System.out.println("jar " + cmdline);