8268613: jar --validate should check inital entries of a JAR file

Reviewed-by: lancea, jvernee
This commit is contained in:
Christian Stein 2025-11-17 07:53:32 +00:00
parent ce1adf63ea
commit 8690d263d9
3 changed files with 111 additions and 0 deletions

View File

@ -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));
}
}
}
}
/**

View File

@ -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

View File

@ -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.
* <p>
* Copied from <code>JarInputStream.java</code>:
* <pre>
* 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.
* </pre>
* 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);