undefect. CWE-407 — 63 sites patched across 27 ecosystems

Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
This commit is contained in:
russell@unturf.com 2026-03-26 17:11:57 -04:00
commit 0a580b313d
70422 changed files with 17213626 additions and 0 deletions

View file

@ -0,0 +1,65 @@
/*
* Copyright (c) 2003, 2026, 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 4842702 8211765
* @summary Check that constructors throw specified exceptions
* @run junit Constructor
*/
import org.junit.jupiter.api.Test;
import java.util.jar.JarFile;
import java.io.File;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class Constructor {
@Test
void constructorTest() {
assertThrows(NullPointerException.class, () -> new JarFile ((File) null, true, JarFile.OPEN_READ));
assertThrows(NullPointerException.class, () -> new JarFile ((File) null, true));
assertThrows(NullPointerException.class, () -> new JarFile ((File) null));
assertThrows(NullPointerException.class, () -> new JarFile ((String) null, true));
assertThrows(NullPointerException.class, () -> new JarFile ((String) null));
assertThrows(IOException.class, () -> new JarFile ("NoSuchJar.jar"));
assertThrows(IOException.class, () -> new JarFile (new File ("NoSuchJar.jar")));
// Test that an IOExcception is thrown when an invalid charater
// is part of the path on Windows and Unix
final String invalidOSPath = System.getProperty("os.name")
.startsWith("Windows") ? "C:\\*" : "foo\u0000bar";
assertThrows(IOException.class, () -> new JarFile (invalidOSPath));
}
}

View file

@ -0,0 +1,394 @@
/*
* Copyright (c) 2023, 2026, 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 8300140
* @summary Make sure signature related files in subdirectories of META-INF are not considered for verification
* @modules java.base/jdk.internal.access
* @modules java.base/sun.security.util
* @modules java.base/sun.security.tools.keytool
* @modules jdk.jartool/sun.security.tools.jarsigner
* @run junit/othervm IgnoreUnrelatedSignatureFiles
*/
import jdk.internal.access.JavaUtilZipFileAccess;
import jdk.internal.access.SharedSecrets;
import jdk.security.jarsigner.JarSigner;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import sun.security.tools.jarsigner.Main;
import sun.security.util.SignatureFileVerifier;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.CodeSigner;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.CertPath;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class IgnoreUnrelatedSignatureFiles {
private static final JavaUtilZipFileAccess JUZA = SharedSecrets.getJavaUtilZipFileAccess();
// This path resides in a subdirectory of META-INF, so it should not be considered signature related
public static final String SUBDIR_SF_PATH = "META-INF/subdirectory/META-INF/SIGNER.SF";
// Jars used for testing. See `setupJars` below for setup
static Path j;
static Path s;
static Path m;
static Path sm;
static Path ca;
static Path cas;
@BeforeAll
static void setupJars() throws Exception {
// Regular signed JAR
j = createJarFile();
s = signJarFile(j, "SIGNER1", "signed");
// Singed JAR with unrelated signature files
m = moveSignatureRelated(s);
sm = signJarFile(m, "SIGNER2", "modified-signed");
// Signed JAR with custom SIG-* files
ca = createCustomAlgJar();
cas = signJarFile(ca, "SIGNER1", "custom-signed");
}
// Sanity check that the basic signed JAR verifies
@Test
void signedJarVerifyTest() throws IOException {
try (JarFile jf = new JarFile(s.toFile(), true)) {
Map<String, Attributes> entries = jf.getManifest().getEntries();
assertEquals(1, entries.size(),
"Expected a single manifest entry for the digest of a.txt, instead found entries: " + entries.keySet());
JarEntry entry = jf.getJarEntry("a.txt");
try (InputStream in = jf.getInputStream(entry)) {
in.transferTo(OutputStream.nullOutputStream());
}
}
}
// Check ZipFile.Source.isSignatureRelated
@Test
void zipFileSourceIsSignatureRelatedTest() throws IOException {
try (JarFile jarFile = new JarFile(m.toFile())) {
List<String> manifestAndSignatureRelatedFiles = JUZA.getManifestAndSignatureRelatedFiles(jarFile);
for (String signatureRelatedFile : manifestAndSignatureRelatedFiles) {
String dir = signatureRelatedFile.substring(0, signatureRelatedFile.lastIndexOf("/"));
assertEquals("META-INF", dir,
"Signature related file does not reside directly in META-INF/ : " + signatureRelatedFile);
}
}
}
// Check SignatureFileVerifier.isSigningRelated
@Test
void sigFileVerifierIsSigningRelatedTest() {
assertFalse(SignatureFileVerifier.isSigningRelated(SUBDIR_SF_PATH),
"Signature related file does not reside directly in META-INF/ : " + SUBDIR_SF_PATH);
}
// Check JarInputStream with doVerify = true
@Test
void jarIStreamDoVerifyTest() throws IOException {
try (JarInputStream in = new JarInputStream(Files.newInputStream(m), true)) {
while (in.getNextEntry() != null) {
in.transferTo(OutputStream.nullOutputStream());
}
}
}
// Check that a JAR containing unrelated .SF, .RSA files is signed as-if it is unsigned
@Test
void unrelatedFilesUnsignedTest() throws IOException {
try (ZipFile zf = new ZipFile(sm.toFile())) {
ZipEntry mf = zf.getEntry("META-INF/MANIFEST.MF");
try (InputStream stream = zf.getInputStream(mf)) {
String manifest = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
// When JarSigner considers a jar to not be already signed,
// the 'Manifest-Version' attributed name will be case-normalized
// Assert that manifest-version is not in lowercase
assertFalse(manifest.startsWith("manifest-version"),
"JarSigner unexpectedly treated unsigned jar as signed");
}
}
}
// Check that a JAR containing non signature related .SF, .RSA files can be signed
@Test
void nonSigFileIsSignableTest() throws Exception {
try (JarFile jf = new JarFile(sm.toFile(), true)) {
checkSignedBy(jf, "a.txt", "CN=SIGNER2");
checkSignedBy(jf, "META-INF/subdirectory/META-INF/SIGNER1.SF", "CN=SIGNER2");
}
}
// Check that JarSigner does not move unrelated [SF,RSA] files to the beginning of signed JARs
@Test
void jarSignerDoesNotMoveUnrelatedTest() throws IOException {
try (JarFile zf = new JarFile(sm.toFile())) {
List<String> actualOrder = zf.stream().map(ZipEntry::getName).toList();
List<String> expectedOrder = List.of(
"META-INF/MANIFEST.MF",
"META-INF/SIGNER2.SF",
"META-INF/SIGNER2.RSA",
"META-INF/subdirectory/META-INF/SIGNER1.SF",
"META-INF/subdirectory/META-INF/SIGNER1.RSA",
"a.txt",
"META-INF/subdirectory2/META-INF/SIGNER1.SF",
"META-INF/subdirectory2/META-INF/SIGNER1.RSA"
);
assertEquals(expectedOrder, actualOrder, ("""
Unexpected file order in JAR with unrelated SF,RSA files
Expected order: %s
Actual order: %s""")
.formatted(expectedOrder, actualOrder));
}
}
// Check that jarsigner ignores unrelated signature files
@Test
void jarSignerIgnoresUnrelatedTest() throws Exception {
String message = jarSignerVerify(m);
assertFalse(message.contains("WARNING"),
"jarsigner output contains unexpected warning: " + message);
}
// Check that SignatureFileVerifier.isSigningRelated handles custom SIG-* files correctly
@Test
void customSIGFilesTest() throws IOException {
try (JarFile jf = new JarFile(cas.toFile(), true)) {
// These files are not signature-related and should be signed
Set<String> expectedSigned = Set.of("a.txt",
"META-INF/unrelated.txt",
"META-INF/SIG-CUSTOM2.C-1",
"META-INF/SIG-CUSTOM2.",
"META-INF/SIG-CUSTOM2.ABCD",
"META-INF/subdirectory/SIG-CUSTOM2.SF",
"META-INF/subdirectory/SIG-CUSTOM2.CS1"
);
Set<String> actualSigned = jf.getManifest().getEntries().keySet();
assertEquals(expectedSigned, actualSigned,
"Unexpected MANIFEST entries. Expected %s, got %s"
.formatted(expectedSigned, actualSigned));
}
}
/**
* run "jarsigner -verify" on the JAR and return the captured output
*/
private static String jarSignerVerify(Path m) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintStream currentOut = System.out;
try {
System.setOut(new PrintStream(out));
Main.main(new String[] {"-verify", m.toAbsolutePath().toString()});
return out.toString(StandardCharsets.UTF_8);
} finally {
System.setOut(currentOut);
}
}
/**
* Check that a path of a given JAR is signed once by the expected signer CN
*/
private static void checkSignedBy(JarFile jf, String name, String expectedSigner) throws Exception {
JarEntry je = jf.getJarEntry(name);
// Read the contents to trigger verification
try (InputStream in = jf.getInputStream(je)) {
in.transferTo(OutputStream.nullOutputStream());
}
// Verify that the entry is signed
CodeSigner[] signers = je.getCodeSigners();
assertNotNull(signers, "Expected %s to be signed".formatted(name));
// There should be a single signer
assertEquals(1, signers.length,
"Expected %s to be signed by exactly one signer".formatted(name));
String actualSigner = ((X509Certificate) signers[0]
.getSignerCertPath().getCertificates().get(0))
.getIssuerX500Principal().getName();
assertEquals(expectedSigner, actualSigner,
"Expected %s to be signed by %s, was signed by %s".formatted(name, expectedSigner, actualSigner));
}
/**
* Create a jar file with a '*.SF' file residing in META-INF/subdirectory/
*/
private static Path createJarFile() throws Exception {
Path jar = Path.of("unrelated-signature-file.jar");
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(jar), manifest)) {
write(out, "a.txt", "a");
}
return jar;
}
private static Path createCustomAlgJar() throws Exception {
Path jar = Path.of("unrelated-signature-file-custom-sig.jar");
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(jar), manifest)) {
// Regular file
write(out, "a.txt", "a");
// Regular file in META-INF
write(out, "META-INF/unrelated.txt", "a");
// Custom SIG files with valid extension (no extension is also OK)
write(out, "META-INF/SIG-CUSTOM.SF", "");
write(out, "META-INF/SIG-CUSTOM.CS1", "");
write(out, "META-INF/SIG-CUSTOM", "");
// Custom SIG files with invalid extensions
write(out, "META-INF/SIG-CUSTOM2.SF", "");
write(out, "META-INF/SIG-CUSTOM2.C-1", "");
write(out, "META-INF/SIG-CUSTOM2.", "");
write(out, "META-INF/SIG-CUSTOM2.ABCD", "");
// Custom SIG files with valid extensions in subdirectories
write(out, "META-INF/subdirectory/SIG-CUSTOM2.SF", "");
write(out, "META-INF/subdirectory/SIG-CUSTOM2.CS1", "");
}
return jar;
}
private static void write(JarOutputStream out, String name, String content) throws IOException {
out.putNextEntry(new JarEntry(name));
out.write(content.getBytes(StandardCharsets.UTF_8));
}
/**
* Create a signed version of the given jar file
*/
private static Path signJarFile(Path jar, String signerName, String classifier) throws Exception {
Path s = Path.of("unrelated-signature-files-" + classifier +".jar");
Files.deleteIfExists(Path.of("ks"));
sun.security.tools.keytool.Main.main(
("-keystore ks -storepass changeit -keypass changeit -dname" +
" CN=" + signerName +" -alias r -genkeypair -keyalg rsa").split(" "));
char[] pass = "changeit".toCharArray();
KeyStore ks = KeyStore.getInstance(
new File("ks"), pass);
PrivateKey pkr = (PrivateKey)ks.getKey("r", pass);
CertPath cp = CertificateFactory.getInstance("X.509")
.generateCertPath(Arrays.asList(ks.getCertificateChain("r")));
JarSigner signer = new JarSigner.Builder(pkr, cp)
.digestAlgorithm("SHA-256")
.signatureAlgorithm("SHA256withRSA")
.signerName(signerName)
.build();
try (ZipFile in = new ZipFile(jar.toFile());
OutputStream out = Files.newOutputStream(s)) {
signer.sign(in, out);
}
return s;
}
/**
* Create a modified version of a signed jar file where signature-related files
* are moved into a subdirectory of META-INF/ and the manifest is changed to trigger
* a digest mismatch.
*
* Since the signature related files are moved out of META-INF/, the returned jar file should
* not be considered signed
*/
private static Path moveSignatureRelated(Path s) throws Exception {
Path m = Path.of("unrelated-signature-files-modified.jar");
try (ZipFile in = new ZipFile(s.toFile());
ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(m))) {
// Change the digest of the manifest by lower-casing the Manifest-Version attribute:
out.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
out.write("manifest-version: 1.0\n\n".getBytes(StandardCharsets.UTF_8));
copy("META-INF/SIGNER1.SF", "META-INF/subdirectory/META-INF/SIGNER1.SF", in, out);
copy("META-INF/SIGNER1.RSA", "META-INF/subdirectory/META-INF/SIGNER1.RSA", in, out);
// Copy over the regular a.txt file
copy("a.txt", "a.txt", in, out);
// These are also just regular files in their new location, but putting them at end
// allows us to verify that JarSigner does not move them to the beginning of the signed JAR
copy("META-INF/SIGNER1.SF", "META-INF/subdirectory2/META-INF/SIGNER1.SF", in, out);
copy("META-INF/SIGNER1.RSA", "META-INF/subdirectory2/META-INF/SIGNER1.RSA", in, out);
}
return m;
}
// Copy a file from a ZipFile into a ZipOutputStream
private static void copy(String from, String to, ZipFile in, ZipOutputStream out) throws Exception {
out.putNextEntry(new ZipEntry(to));
try (InputStream zi = in.getInputStream(new ZipEntry(from))) {
zi.transferTo(out);
}
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2017, 2026, 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 8186334
* @summary Make sure scanning manifest doesn't throw AIOOBE on certain strings containing backticks.
* @library /test/lib/
* @build jdk.test.lib.util.JarBuilder
* @run junit JarBacktickManifest
*/
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.jar.JarFile;
import jdk.test.lib.util.JarBuilder;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
public class JarBacktickManifest {
public static final String VERIFY_MANIFEST_JAR = "verifyManifest.jar";
@BeforeAll
public static void initialize() throws Exception {
JarBuilder jb = new JarBuilder(VERIFY_MANIFEST_JAR);
jb.addAttribute("Test", " Class-`Path` ");
jb.addAttribute("Test2", " Multi-`Release ");
jb.build();
}
@Test
public void backtickTest() throws Exception {
try (JarFile jf = new JarFile(VERIFY_MANIFEST_JAR)) { // do not set runtime versioning
assertFalse(jf.isMultiRelease(), "Shouldn't be multi-release");
}
}
@AfterAll
public static void close() throws IOException {
Files.delete(new File(VERIFY_MANIFEST_JAR).toPath());
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2002, 2026, 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 4771616
@summary JarFile.maybeInstantiateVerifier must check for absence of manifest
@run junit JarNoManifest
*/
import org.junit.jupiter.api.Test;
import java.io.*;
import java.util.jar.*;
import java.util.zip.*;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
public class JarNoManifest {
@Test
void absentManifestTest() throws IOException {
File f = new File(System.getProperty("test.src", "."), "no-manifest.jar");
JarFile jar = new JarFile(f);
ZipEntry entry = jar.getEntry("JarNoManifest.java");
// The following throws a NullPointerException when the bug is present
assertDoesNotThrow(() -> jar.getInputStream(entry));
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2011, 2026, 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 7023056
* @summary NPE from sun.security.util.ManifestEntryVerifier.verify during Maven build
* @run junit MevNPE
*/
import org.junit.jupiter.api.Test;
import java.io.*;
import java.util.jar.*;
public class MevNPE {
@Test
void noNpeTest() throws IOException {
File f = new File(System.getProperty("test.src", "."), "Signed.jar");
try (JarFile jf = new JarFile(f, true)) {
try (InputStream s1 = jf.getInputStream(
jf.getJarEntry(JarFile.MANIFEST_NAME))) {
s1.read(new byte[10000]);
}
try (InputStream s2 = jf.getInputStream(
jf.getJarEntry(JarFile.MANIFEST_NAME))) {
s2.read(new byte[10000]);
}
}
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2003, 2026, 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 4953126
* @summary Check that a signed JAR file containing an unsupported signer info
* attribute can be parsed successfully.
* @run junit ScanSignedJar
*/
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.jar.*;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ScanSignedJar {
@Test
void unsupportedSignerTest() throws IOException {
boolean isSigned = false;
try (JarFile file = new JarFile(new File(System.getProperty("test.src","."),
"bogus-signerinfo-attr.jar"))) {
byte[] buffer = new byte[8192];
for (Enumeration<JarEntry> entries = file.entries(); entries.hasMoreElements();) {
JarEntry entry = entries.nextElement();
try (InputStream jis = file.getInputStream(entry)) {
while (jis.read(buffer, 0, buffer.length) != -1) {
// read the jar entry
}
}
if (entry.getCertificates() != null) {
isSigned = true;
}
System.out.println((isSigned ? "[signed] " : "\t ") +
entry.getName());
}
}
assertTrue(isSigned, "Failed to detect that the JAR file is signed");
}
}

Binary file not shown.

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2004, 2026, 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 4845692 8206863
* @summary JarFile.getInputStream should not throw when jar file is signed
* @run junit SignedJarFileGetInputStream
*/
import org.junit.jupiter.api.Test;
import java.io.*;
import java.util.*;
import java.util.jar.*;
import java.util.zip.*;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class SignedJarFileGetInputStream {
@Test
void signedJarTest() throws IOException {
JarFile jar = new JarFile(
new File(System.getProperty("test.src", "."), "Signed.jar"));
for (Enumeration e = jar.entries(); e.hasMoreElements();) {
JarEntry entry = (JarEntry) e.nextElement();
InputStream is = assertDoesNotThrow(() -> jar.getInputStream(new ZipEntry(entry.getName())));
is.close();
}
// read(), available() on closed stream should throw IOException
InputStream is = jar.getInputStream(new ZipEntry("Test.class"));
is.close();
byte[] buffer = new byte[1];
assertThrows(IOException.class, () -> is.read());
assertThrows(IOException.class, () -> is.read(buffer));
assertThrows(IOException.class, () -> is.read(buffer, 0, buffer.length));
assertThrows(IOException.class, () -> is.available());
}
}

View file

@ -0,0 +1,194 @@
/*
* Copyright (c) 2023, 2026, 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
* @modules java.base/sun.security.tools.keytool
* @summary JARs with pending block files (where .RSA comes before .SF) should verify correctly
* @run junit SignedJarPendingBlock
*/
import jdk.security.jarsigner.JarSigner;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.FieldSource;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.util.Collections;
import java.util.jar.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class SignedJarPendingBlock {
static Path signed;
static Path pendingBlocks;
static Path invalid;
// Construct the test data
@BeforeAll
static void setup() throws Exception {
Path jar = createJarFile();
signed = signJarFile(jar);
pendingBlocks = moveBlockFirst(signed);
invalid = invalidate(pendingBlocks);
}
// Regular signed JAR with no pending blocks should verify
@Test
void checkValidSignedJar() {
assertDoesNotThrow(() -> checkSigned(signed),
"Valid digest should not fail");
}
// Signed jar with pending blocks should verify
@Test
void checkValidSignedPendingJar() {
assertDoesNotThrow(() -> checkSigned(pendingBlocks),
"Valid digest should not fail");
}
// Invalid signed jar with pending blocks should throw SecurityException
@Test
void checkInvalidSignedJar() {
assertThrows(SecurityException.class, () -> checkSigned(invalid),
"Expected invalid digest to be detected");
}
private static void checkSigned(Path b) throws IOException {
try (JarFile jf = new JarFile(b.toFile(), true)) {
JarEntry je = jf.getJarEntry("a.txt");
try (InputStream in = jf.getInputStream(je)) {
in.transferTo(OutputStream.nullOutputStream());
}
}
}
/**
* Invalidate signed file by modifying the contents of "a.txt"
*/
private static Path invalidate(Path s) throws Exception{
Path invalid = Path.of("pending-block-file-invalidated.jar");
try (ZipFile zip = new ZipFile(s.toFile());
ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(invalid))) {
for (ZipEntry ze : Collections.list(zip.entries())) {
String name = ze.getName();
out.putNextEntry(new ZipEntry(name));
if (name.equals("a.txt")) {
// Change the contents of a.txt to trigger SignatureException
out.write("b".getBytes(StandardCharsets.UTF_8));
} else {
try (InputStream in = zip.getInputStream(ze)) {
in.transferTo(out);
}
}
}
}
return invalid;
}
private static Path moveBlockFirst(Path s) throws Exception {
Path b = Path.of("pending-block-file-blockfirst.jar");
try (ZipFile in = new ZipFile(s.toFile());
ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(b))) {
copy("META-INF/MANIFEST.MF", in, out);
// Switch the order of the RSA and SF files
copy("META-INF/SIGNER.RSA", in, out);
copy("META-INF/SIGNER.SF", in, out);
copy("a.txt", in, out);
}
return b;
}
/**
* Copy an entry from a ZipFile to a ZipOutputStream
*/
private static void copy(String name, ZipFile in, ZipOutputStream out) throws Exception {
out.putNextEntry(new ZipEntry(name));
try (InputStream is = in.getInputStream(in.getEntry(name))) {
is.transferTo(out);
}
}
private static Path signJarFile(Path j) throws Exception {
Path s = Path.of("pending-block-file-signed.jar");
Files.deleteIfExists(Path.of("ks"));
sun.security.tools.keytool.Main.main(
("-keystore ks -storepass changeit -keypass changeit -dname" +
" CN=SIGNER" +" -alias r -genkeypair -keyalg rsa").split(" "));
char[] pass = "changeit".toCharArray();
KeyStore ks = KeyStore.getInstance(new File("ks"), pass);
KeyStore.PrivateKeyEntry pke = (KeyStore.PrivateKeyEntry)
ks.getEntry("r", new KeyStore.PasswordProtection(pass));
JarSigner signer = new JarSigner.Builder(pke)
.digestAlgorithm("SHA-256")
.signatureAlgorithm("SHA256withRSA")
.signerName("SIGNER")
.build();
try (ZipFile in = new ZipFile(j.toFile());
OutputStream out = Files.newOutputStream(s)) {
signer.sign(in, out);
}
return s;
}
/**
* Create a jar file with single entry "a.txt" containing "a"
*/
private static Path createJarFile() throws Exception {
Path jar = Path.of("pending-block-file.jar");
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(jar),manifest)) {
out.putNextEntry(new JarEntry("a.txt"));
out.write("a".getBytes(StandardCharsets.UTF_8));
}
return jar;
}
}

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2003, 2026, 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 4910572
* @summary Accessing a closed jar file should generate IllegalStateException.
* @run junit SorryClosed
*/
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.File;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class SorryClosed {
private static final File file = new File(System.getProperty("test.src", "."), "test.jar");
private static final String testEntryName = "test.class";
@Test
void getInputStreamTest() throws IOException {
JarFile f = new JarFile(file);
ZipEntry e = f.getEntry(testEntryName);
f.close();
assertThrows(IllegalStateException.class, () -> f.getInputStream(e));
}
@Test
void getEntryTest() throws IOException {
JarFile f = new JarFile(file);
f.close();
assertThrows(IllegalStateException.class, () -> f.getEntry(testEntryName));
}
@Test
void getJarEntryTest() throws IOException {
JarFile f = new JarFile(file);
f.close();
assertThrows(IllegalStateException.class, () -> f.getJarEntry(testEntryName));
}
@Test
void getManifestTest() throws IOException {
JarFile f = new JarFile(file);
f.close();
assertThrows(IllegalStateException.class, f::getManifest);
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2002, 2026, 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 4624534
* @summary Make sure jar certificates work for Turkish locale
* @run junit/othervm -Duser.language=tr -Duser.country=TR TurkCert
*/
import org.junit.jupiter.api.Test;
import java.util.*;
import java.util.jar.*;
import java.security.cert.*;
import java.io.*;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class TurkCert {
@Test
void turkishLocaleTest() throws IOException {
File f = new File(System.getProperty("test.src", "."), "test.jar");
try (JarFile jf = new JarFile(f, true)) {
JarEntry je = (JarEntry)jf.getEntry("test.class");
try (InputStream is = jf.getInputStream(je)) {
byte[] b = new byte[1024];
while (is.read(b) != -1) {
}
}
assertNotNull(je.getCertificates(), "Null certificate for test.class.");
}
}
}

View file

@ -0,0 +1,139 @@
/*
* Copyright (c) 2001, 2026, 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
* @modules java.base/sun.security.x509
* @modules java.base/sun.security.tools.keytool
* @bug 4419266 4842702
* @summary Make sure verifying signed Jar doesn't throw SecurityException
* @run junit VerifySignedJar
*/
import jdk.security.jarsigner.JarSigner;
import org.junit.jupiter.api.Test;
import sun.security.tools.keytool.CertAndKeyGen;
import sun.security.x509.X500Name;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipFile;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class VerifySignedJar {
@Test
void signedJarSecurityExceptionTest() throws Exception {
Path j = createJar();
Path s = signJar(j, keyEntry("cn=duke"));
try (JarFile jf = new JarFile(s.toFile())) {
for (JarEntry e: Collections.list(jf.entries())) {
// Reading entry to trigger verification
jf.getInputStream(e).transferTo(OutputStream.nullOutputStream());
// Check that all regular files are signed by duke
if (!e.getName().startsWith("META-INF/")) {
checkSignedBy(e, "cn=duke");
}
}
// Read ZIP and JAR entries by name
assertNotNull(jf.getEntry("getprop.class"));
assertNotNull(jf.getJarEntry("getprop.class"));
// Make sure we throw NPE on null parameters
assertThrows(NullPointerException.class, () -> jf.getEntry(null));
assertThrows(NullPointerException.class, () -> jf.getJarEntry(null));
assertThrows(NullPointerException.class, () -> jf.getInputStream(null));
} catch (SecurityException se) {
fail("Got SecurityException when verifying signed jar:" + se);
}
}
// Check that a JAR entry is signed by an expected DN
private static void checkSignedBy(JarEntry e, String expectedDn) {
Certificate[] certs = e.getCertificates();
assertNotNull(certs, "JarEntry has no certificates: " + e.getName());
assertNotEquals(0, certs.length, "JarEntry has no certificates: " + e.getName());
var x = assertInstanceOf(X509Certificate.class, certs[0], "Expected JarEntry.getCertificate to return X509Certificate");
String name = x.getSubjectX500Principal().getName();
assertTrue(name.equalsIgnoreCase(expectedDn), "Expected entry signed by %s, was %s".formatted(name, expectedDn));
}
private static Path createJar() throws IOException {
Path j = Path.of("unsigned.jar");
try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(j))){
out.putNextEntry(new JarEntry("getprop.class"));
out.write(new byte[] {(byte) 0XCA, (byte) 0XFE, (byte) 0XBA, (byte) 0XBE});
}
return j;
}
private static Path signJar(Path j, KeyStore.PrivateKeyEntry entry) throws Exception {
Path s = Path.of("signed.jar");
JarSigner signer = new JarSigner.Builder(entry)
.signerName("zigbert")
.digestAlgorithm("SHA-256")
.signatureAlgorithm("SHA256withRSA")
.build();
try (ZipFile zip = new ZipFile(j.toFile());
OutputStream out = Files.newOutputStream(s)) {
signer.sign(zip, out);
}
return s;
}
private static KeyStore.PrivateKeyEntry keyEntry(String dname) throws Exception {
CertAndKeyGen gen = new CertAndKeyGen("RSA", "SHA256withRSA");
gen.generate(1048); // Small key size makes test run faster
var oneDay = TimeUnit.DAYS.toSeconds(1);
Certificate cert = gen.getSelfCertificate(new X500Name(dname), oneDay);
return new KeyStore.PrivateKeyEntry(gen.getPrivateKey(),
new Certificate[] {cert});
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020, 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.
*/
public abstract class FooService { }

View file

@ -0,0 +1,188 @@
/*
* Copyright (c) 2020, 2026, 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 8234466
* @summary attempt to trigger class loading from the classloader
* during JAR file verification
* @library /test/lib
* @build jdk.test.lib.compiler.CompilerUtils
* jdk.test.lib.process.*
* jdk.test.lib.util.JarUtils
* jdk.test.lib.JDKToolLauncher
* MultiThreadLoad FooService
* @modules java.base/jdk.internal.access:+open
* @run junit MultiProviderTest
*/
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import jdk.test.lib.JDKToolFinder;
import jdk.test.lib.JDKToolLauncher;
import jdk.test.lib.Utils;
import jdk.test.lib.compiler.CompilerUtils;
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import jdk.test.lib.util.JarUtils;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
public class MultiProviderTest {
private static final String METAINFO = "META-INF/services/FooService";
private static String TEST_CLASS_PATH = System.getProperty("test.classes", ".");
static final int NUM_JARS = 5;
// Reset per each test run under JUnit default lifecycle
private boolean signJars = false;
private String COMBO_CP = Utils.TEST_CLASS_PATH + File.pathSeparator;
private static final String KEYSTORE = "keystore.jks";
private static final String ALIAS = "JavaTest";
private static final String STOREPASS = "changeit";
private static final String KEYPASS = "changeit";
@ParameterizedTest
@ValueSource(booleans = {true, false})
void classLoadingTest(boolean sign) throws Throwable {
signJars = sign;
initialize();
List<String> cmds = new ArrayList<>();
cmds.add(JDKToolFinder.getJDKTool("java"));
cmds.addAll(asList(Utils.getTestJavaOpts()));
cmds.addAll(List.of(
"-cp",
COMBO_CP,
"--add-opens",
"java.base/jdk.internal.access=ALL-UNNAMED",
"-Djava.util.logging.config.file=" +
Path.of(System.getProperty("test.src", "."), "logging.properties").toString(),
"MultiThreadLoad",
TEST_CLASS_PATH));
assertDoesNotThrow(() -> {
OutputAnalyzer outputAnalyzer = ProcessTools.executeCommand(cmds.stream()
.filter(t -> !t.isEmpty())
.toArray(String[]::new))
.shouldHaveExitValue(0);
System.out.println("Output:" + outputAnalyzer.getOutput());
});
}
public void initialize() throws Throwable {
if (signJars) {
genKey();
}
for (int i = 0; i < NUM_JARS; i++) {
String p = "FooProvider" + i;
String jarName = "FooProvider" + i + ".jar";
Path javaPath = Path.of(p + ".java");
Path jarPath = Path.of(p + ".jar");
String contents = "public class FooProvider" + i + " extends FooService { }";
Files.write(javaPath, contents.getBytes());
CompilerUtils.compile(javaPath, Path.of(System.getProperty("test.classes")), "-cp", Utils.TEST_CLASS_PATH);
createJar(jarPath, p, List.of(p));
if (signJars) {
signJar(TEST_CLASS_PATH + File.separator + jarName);
}
COMBO_CP += TEST_CLASS_PATH + File.separator + jarName + File.pathSeparator;
}
}
private static void createProviderConfig(Path config, String providerName) throws Exception {
Files.createDirectories(config.getParent());
Files.write(config, providerName.getBytes(), CREATE);
}
private static void createJar(Path jar, String provider, List<String> files) throws Exception {
Path xdir = Path.of(provider);
createProviderConfig(xdir.resolve(METAINFO), provider);
for (String f : files) {
Path source = Path.of(Utils.TEST_CLASSES, f + ".class");
Path target = xdir.resolve(source.getFileName());
Files.copy(source, target, REPLACE_EXISTING);
}
JarUtils.createJarFile(Path.of(TEST_CLASS_PATH, jar.getFileName().toString()), xdir);
}
private static void genKey() throws Throwable {
String keytool = JDKToolFinder.getJDKTool("keytool");
Files.deleteIfExists(Paths.get(KEYSTORE));
ProcessTools.executeCommand(keytool,
"-J-Duser.language=en",
"-J-Duser.country=US",
"-genkey",
"-keyalg", "rsa",
"-alias", ALIAS,
"-keystore", KEYSTORE,
"-keypass", KEYPASS,
"-dname", "cn=sample",
"-storepass", STOREPASS
).shouldHaveExitValue(0);
}
private static OutputAnalyzer signJar(String jarName) throws Throwable {
List<String> args = new ArrayList<>();
args.add("-verbose");
args.add(jarName);
args.add(ALIAS);
return jarsigner(args);
}
private static OutputAnalyzer jarsigner(List<String> extra)
throws Throwable {
JDKToolLauncher launcher = JDKToolLauncher.createUsingTestJDK("jarsigner")
.addVMArg("-Duser.language=en")
.addVMArg("-Duser.country=US")
.addToolArg("-keystore")
.addToolArg(KEYSTORE)
.addToolArg("-storepass")
.addToolArg(STOREPASS)
.addToolArg("-keypass")
.addToolArg(KEYPASS);
for (String s : extra) {
if (s.startsWith("-J")) {
launcher.addVMArg(s.substring(2));
} else {
launcher.addToolArg(s);
}
}
return ProcessTools.executeCommand(launcher.getCommand());
}
}

View file

@ -0,0 +1,128 @@
/*
* Copyright (c) 2020, 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.
*/
import jdk.internal.access.JavaUtilJarAccess;
import jdk.internal.access.SharedSecrets;
import java.io.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.jar.JarFile;
public class MultiThreadLoad {
private static PrintStream out = System.err;
static String TEST_CLASS_PATH;
private static final JavaUtilJarAccess JUJA = SharedSecrets.javaUtilJarAccess();
private static CountDownLatch cdl = new CountDownLatch(1);
private static <T> Set<T> setOf(Iterable<T> it) {
Set<T> s = new HashSet<T>();
for (T t : it)
s.add(t);
return s;
}
private static <T> void checkEquals(Set<T> s1, Set<T> s2, boolean eq) {
if (s1.equals(s2) != eq)
throw new RuntimeException(String.format("%b %s : %s",
eq, s1, s2));
}
abstract static class TestLoader {
String name;
TestLoader(String name) { this.name = name; }
abstract ServiceLoader<FooService> load();
}
static TestLoader tcclLoader = new TestLoader("Thread context class loader") {
ServiceLoader<FooService> load() {
return ServiceLoader.load(FooService.class);
}
};
static TestLoader systemClLoader = new TestLoader("System class loader") {
ServiceLoader<FooService> load() {
return ServiceLoader.load(FooService.class, ClassLoader.getSystemClassLoader());
}
};
static TestLoader nullClLoader = new TestLoader("null (defer to system class loader)") {
ServiceLoader<FooService> load() {
return ServiceLoader.load(FooService.class, null);
}
};
public static void main(String[] args) {
// keep reference to variables for the newly launced process
TEST_CLASS_PATH = args[0];
for (TestLoader tl : Arrays.asList(tcclLoader, systemClLoader, nullClLoader)) {
test(tl);
}
}
static void test(TestLoader tl) {
Runnable r1 = () -> {
ServiceLoader<FooService> sl = tl.load();
out.format("%s: %s%n", tl.name, sl);
// Providers are cached
Set<FooService> ps = setOf(sl);
cdl.countDown();
checkEquals(ps, setOf(sl), true);
// The cache can be flushed and reloaded
sl.reload();
checkEquals(ps, setOf(sl), false);
};
Runnable r2 = () -> {
jarCrawler(Path.of(TEST_CLASS_PATH));
};
new Thread(r2).start();
new Thread(r1).start();
}
private static void jarCrawler(Path p) {
try {
// let the other thread spin up
cdl.await();
} catch (InterruptedException e) {
// ignore
}
try {
for (int i = MultiProviderTest.NUM_JARS -1; i >= 0; i--) {
JUJA.ensureInitialization(new JarFile(TEST_CLASS_PATH + File.separator
+ "FooProvider" + i + ".jar"));
}
} catch (Exception e) {
System.out.println("Exception during jar crawl: ");
e.printStackTrace(System.out);
}
}
}

View file

@ -0,0 +1,13 @@
############################################################
# Configuration file for log testing
#
############################################################
handlers= java.util.logging.ConsoleHandler
.level= FINE
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
jdk.event.security.level = FINE

View file

@ -0,0 +1,258 @@
/*
* Copyright (c) 2015, 2026, 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 8132734 8144062 8165723 8199172
* @summary Test the extended API and the aliasing additions in JarFile that
* support multi-release jar files
* @library /lib/testlibrary/java/util/jar /test/lib
* @build jdk.test.lib.RandomFactory
* CreateMultiReleaseTestJars
* jdk.test.lib.compiler.Compiler
* jdk.test.lib.util.JarBuilder
* @run junit MultiReleaseJarAPI
*/
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.JarFile;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import jdk.test.lib.RandomFactory;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.*;
public class MultiReleaseJarAPI {
private static final Random RANDOM = RandomFactory.getRandom();
private static final String userdir = System.getProperty("user.dir", ".");
private static final CreateMultiReleaseTestJars creator = new CreateMultiReleaseTestJars();
private static final File unversioned = new File(userdir, "unversioned.jar");
private static final File multirelease = new File(userdir, "multi-release.jar");
private static final File signedmultirelease = new File(userdir, "signed-multi-release.jar");
@BeforeAll
public static void initialize() throws Exception {
creator.compileEntries();
creator.buildUnversionedJar();
creator.buildMultiReleaseJar();
creator.buildSignedMultiReleaseJar();
}
@AfterAll
public static void close() throws IOException {
Files.delete(unversioned.toPath());
Files.delete(multirelease.toPath());
Files.delete(signedmultirelease.toPath());
}
@Test
public void isMultiReleaseJar() throws Exception {
try (JarFile jf = new JarFile(unversioned)) {
assertFalse(jf.isMultiRelease());
}
try (JarFile jf = new JarFile(unversioned, true, ZipFile.OPEN_READ, Runtime.version())) {
assertFalse(jf.isMultiRelease());
}
try (JarFile jf = new JarFile(multirelease)) {
assertTrue(jf.isMultiRelease());
}
try (JarFile jf = new JarFile(multirelease, true, ZipFile.OPEN_READ, Runtime.version())) {
assertTrue(jf.isMultiRelease());
}
testCustomMultiReleaseValue("true", true);
testCustomMultiReleaseValue("true\r\nOther: value", true);
testCustomMultiReleaseValue("true\nOther: value", true);
testCustomMultiReleaseValue("true\rOther: value", true);
testCustomMultiReleaseValue("false", false);
testCustomMultiReleaseValue(" true", false);
testCustomMultiReleaseValue("true ", false);
testCustomMultiReleaseValue("true\n true", false);
testCustomMultiReleaseValue("true\r true", false);
testCustomMultiReleaseValue("true\r\n true", false);
// "Multi-Release: true/false" not in main attributes
testCustomMultiReleaseValue("\r\n\r\nName: test\r\nMulti-Release: true\r\n",
false);
testCustomMultiReleaseValue("\n\nName: entryname\nMulti-Release: true\n",
false);
testCustomMultiReleaseValue("EndOfMainAttr: whatever\r\n" +
"\r\nName: entryname\r\nMulti-Release: true\r\n",
false);
testCustomMultiReleaseValue("EndOfMainAttr: whatever\r\n" +
"\nName: entryname\nMulti-Release: true\n",
false);
// generate "random" Strings to use as extra attributes, and
// verify that Multi-Release: true is always properly matched
for (int i = 0; i < 100; i++) {
byte[] keyBytes = new byte[RANDOM.nextInt(70) + 1];
Arrays.fill(keyBytes, (byte)('a' + RANDOM.nextInt(24)));
byte[] valueBytes = new byte[RANDOM.nextInt(70) + 1];
Arrays.fill(valueBytes, (byte)('a' + RANDOM.nextInt(24)));
String key = new String(keyBytes, StandardCharsets.UTF_8);
String value = new String(valueBytes, StandardCharsets.UTF_8);
// test that Multi-Release: true anywhere in the manifest always
// return true
testCustomMultiReleaseValue("true", Map.of(key, value), true);
// test that we don't get any false positives
testCustomMultiReleaseValue("false", Map.of(key, value), false);
}
}
private void testCustomMultiReleaseValue(String value, boolean expected)
throws Exception {
testCustomMultiReleaseValue(value, Map.of(), expected);
}
private static final AtomicInteger JAR_COUNT = new AtomicInteger(0);
private void testCustomMultiReleaseValue(String value,
Map<String, String> extraAttributes, boolean expected)
throws Exception {
String fileName = "custom-mr" + JAR_COUNT.incrementAndGet() + ".jar";
creator.buildCustomMultiReleaseJar(fileName, value, extraAttributes);
File custom = new File(userdir, fileName);
try (JarFile jf = new JarFile(custom, true, ZipFile.OPEN_READ, Runtime.version())) {
assertEquals(expected, jf.isMultiRelease());
}
Files.delete(custom.toPath());
}
public static Stream<Arguments> createVersionData() {
return Stream.of(
Arguments.of(JarFile.baseVersion(), 8),
Arguments.of(JarFile.runtimeVersion(), Runtime.version().major()),
Arguments.of(Runtime.version(), Runtime.version().major()),
Arguments.of(Runtime.Version.parse("7.1"), JarFile.baseVersion().major()),
Arguments.of(Runtime.Version.parse("9"), 9),
Arguments.of(Runtime.Version.parse("9.1.5-ea+200"), 9)
);
}
@ParameterizedTest
@MethodSource("createVersionData")
public void testVersioning(Runtime.Version value, int xpected) throws Exception {
Runtime.Version expected = Runtime.Version.parse(String.valueOf(xpected));
Runtime.Version base = JarFile.baseVersion();
// multi-release jar, opened as unversioned
try (JarFile jar = new JarFile(multirelease)) {
assertEquals(base, jar.getVersion());
}
System.err.println("test versioning for Release " + value);
try (JarFile jf = new JarFile(multirelease, true, ZipFile.OPEN_READ, value)) {
assertEquals(expected, jf.getVersion());
}
// regular, unversioned, jar
try (JarFile jf = new JarFile(unversioned, true, ZipFile.OPEN_READ, value)) {
assertEquals(base, jf.getVersion());
}
}
@ParameterizedTest
@MethodSource("createVersionData")
public void testAliasing(Runtime.Version version, int xpected) throws Exception {
int n = Math.max(version.major(), JarFile.baseVersion().major());
Runtime.Version value = Runtime.Version.parse(String.valueOf(n));
System.err.println("test aliasing for Release " + version);
String prefix;
if (JarFile.baseVersion().equals(value)) {
prefix = "";
} else {
prefix = "META-INF/versions/" + value.major() + "/";
}
// test both multi-release jars
readAndCompare(multirelease, value, "README", prefix + "README");
readAndCompare(multirelease, value, "version/Version.class", prefix + "version/Version.class");
// and signed multi-release jars
readAndCompare(signedmultirelease, value, "README", prefix + "README");
readAndCompare(signedmultirelease, value, "version/Version.class", prefix + "version/Version.class");
}
private void readAndCompare(File jar, Runtime.Version version, String name, String realName) throws Exception {
byte[] baseBytes;
byte[] versionedBytes;
try (JarFile jf = new JarFile(jar, true, ZipFile.OPEN_READ, JarFile.baseVersion())) {
ZipEntry ze = jf.getEntry(realName);
try (InputStream is = jf.getInputStream(ze)) {
baseBytes = is.readAllBytes();
}
}
assert baseBytes.length > 0;
try (JarFile jf = new JarFile(jar, true, ZipFile.OPEN_READ, version)) {
ZipEntry ze = jf.getEntry(name);
try (InputStream is = jf.getInputStream(ze)) {
versionedBytes = is.readAllBytes();
}
}
assert versionedBytes.length > 0;
assertTrue(Arrays.equals(baseBytes, versionedBytes));
}
@Test
public void testNames() throws Exception {
String rname = "version/Version.class";
String vname = "META-INF/versions/9/version/Version.class";
ZipEntry ze1;
ZipEntry ze2;
try (JarFile jf = new JarFile(multirelease)) {
ze1 = jf.getEntry(vname);
}
assertEquals(vname, ze1.getName());
try (JarFile jf = new JarFile(multirelease, true, ZipFile.OPEN_READ, Runtime.Version.parse("9"))) {
ze2 = jf.getEntry(rname);
}
assertEquals(rname, ze2.getName());
assertNotEquals(ze2.getName(), ze1.getName());
}
}

View file

@ -0,0 +1,125 @@
/*
* Copyright (c) 2015, 2026, 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 8132734 8194070
* @summary Test the System properties for JarFile that support multi-release jar files
* @library /lib/testlibrary/java/util/jar /test/lib
* @modules jdk.jartool
* jdk.compiler
* @build CreateMultiReleaseTestJars
* jdk.test.lib.compiler.Compiler
* jdk.test.lib.util.JarBuilder
* @run junit MultiReleaseJarHttpProperties
* @run junit/othervm -Djdk.util.jar.version=0 MultiReleaseJarHttpProperties
* @run junit/othervm -Djdk.util.jar.version=8 MultiReleaseJarHttpProperties
* @run junit/othervm -Djdk.util.jar.version=9 MultiReleaseJarHttpProperties
* @run junit/othervm -Djdk.util.jar.version=100 MultiReleaseJarHttpProperties
* @run junit/othervm -Djdk.util.jar.version=8 -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarHttpProperties
* @run junit/othervm -Djdk.util.jar.version=9 -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarHttpProperties
* @run junit/othervm -Djdk.util.jar.version=8 -Djdk.util.jar.enableMultiRelease=force MultiReleaseJarHttpProperties
* @run junit/othervm -Djdk.util.jar.version=9 -Djdk.util.jar.enableMultiRelease=force MultiReleaseJarHttpProperties
* @run junit/othervm -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarHttpProperties
* @run junit/othervm -Djdk.util.jar.enableMultiRelease=force MultiReleaseJarHttpProperties
*/
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.SimpleFileServer;
import jdk.test.lib.net.URIBuilder;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class MultiReleaseJarHttpProperties extends MultiReleaseJarProperties {
private HttpServer server;
private ExecutorService executor;
static final String TESTCONTEXT = "/multi-release.jar"; //mapped to local file path
@BeforeAll
public void initialize() throws Exception {
server = SimpleFileServer.createFileServer(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0),
Path.of(System.getProperty("user.dir", ".")), SimpleFileServer.OutputLevel.INFO);
executor = Executors.newCachedThreadPool();
server.setExecutor(executor);
server.start();
super.initialize();
}
@Override
protected void initializeClassLoader() throws Exception {
URL[] urls = new URL[]{
URIBuilder.newBuilder().scheme("http").port(server.getAddress().getPort()).loopback()
.path(TESTCONTEXT).toURL(),
};
cldr = new URLClassLoader(urls);
// load any class, Main is convenient and in the root entries
rootClass = cldr.loadClass("version.Main");
}
@AfterAll
public void close() throws IOException {
// Windows requires server to stop before file is deleted
if (server != null) {
server.stop(0);
executor.shutdown();
}
super.close();
}
/*
* jdk.util.jar.enableMultiRelease=force is a no-op for URLClassLoader
*/
@Test
public void testURLClassLoader() throws Throwable {
Class<?> vcls = cldr.loadClass("version.Version");
invokeMethod(vcls, rtVersion);
}
@Test
public void testGetResourceAsStream() throws Exception {
String resource = rtVersion == 9 ? "/version/PackagePrivate.java" : "/version/Version.java";
// use rootClass as a base for getting resources
getResourceAsStream(rootClass, resource);
}
@Test
public void testGetResource() throws Exception {
String resource = rtVersion == 9 ? "/version/PackagePrivate.java" : "/version/Version.java";
// use rootClass as a base for getting resources
getResource(rootClass, resource);
}
}

View file

@ -0,0 +1,202 @@
/*
* Copyright (c) 2015, 2026, 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 8132734 8144062 8194070
* @summary Test the System properties for JarFile that support multi-release jar files
* @library /lib/testlibrary/java/util/jar /test/lib/
* @build CreateMultiReleaseTestJars
* jdk.test.lib.compiler.Compiler
* jdk.test.lib.util.JarBuilder
* @run junit MultiReleaseJarProperties
* @run junit/othervm -Djdk.util.jar.version=0 MultiReleaseJarProperties
* @run junit/othervm -Djdk.util.jar.version=8 MultiReleaseJarProperties
* @run junit/othervm -Djdk.util.jar.version=9 MultiReleaseJarProperties
* @run junit/othervm -Djdk.util.jar.version=100 MultiReleaseJarProperties
* @run junit/othervm -Djdk.util.jar.version=8 -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarProperties
* @run junit/othervm -Djdk.util.jar.version=9 -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarProperties
* @run junit/othervm -Djdk.util.jar.version=8 -Djdk.util.jar.enableMultiRelease=force MultiReleaseJarProperties
* @run junit/othervm -Djdk.util.jar.version=9 -Djdk.util.jar.enableMultiRelease=force MultiReleaseJarProperties
* @run junit/othervm -Djdk.util.jar.enableMultiRelease=false MultiReleaseJarProperties
* @run junit/othervm -Djdk.util.jar.enableMultiRelease=force MultiReleaseJarProperties
*/
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import static org.junit.jupiter.api.Assertions.*;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class MultiReleaseJarProperties {
final static int BASE_VERSION = JarFile.baseVersion().major();
final static String userdir = System.getProperty("user.dir", ".");
final static File multirelease = new File(userdir, "multi-release.jar");
protected int rtVersion;
boolean force;
protected ClassLoader cldr;
protected Class<?> rootClass;
@BeforeAll
public void initialize() throws Exception {
CreateMultiReleaseTestJars creator = new CreateMultiReleaseTestJars();
creator.compileEntries();
creator.buildMultiReleaseJar();
int RUNTIME_VERSION = Runtime.version().major();
rtVersion = Integer.getInteger("jdk.util.jar.version", RUNTIME_VERSION);
String mrprop = System.getProperty("jdk.util.jar.enableMultiRelease", "");
if (mrprop.equals("false")) {
rtVersion = BASE_VERSION;
} else if (rtVersion < BASE_VERSION) {
rtVersion = BASE_VERSION;
} else if (rtVersion > RUNTIME_VERSION) {
rtVersion = RUNTIME_VERSION;
}
force = mrprop.equals("force");
initializeClassLoader();
}
protected void initializeClassLoader() throws Exception {
URL[] urls = new URL[]{multirelease.toURI().toURL()};
cldr = new URLClassLoader(urls);
// load any class, Main is convenient and in the root entries
rootClass = cldr.loadClass("version.Main");
}
@AfterAll
public void close() throws IOException {
((URLClassLoader) cldr).close();
Files.delete(multirelease.toPath());
}
/*
* jdk.util.jar.enableMultiRelease=force is a no-op for URLClassLoader
*/
@Test
public void testURLClassLoader() throws Throwable {
Class<?> vcls = cldr.loadClass("version.Version");
invokeMethod(vcls, rtVersion);
}
protected void invokeMethod(Class<?> vcls, int expected) throws Throwable {
MethodType mt = MethodType.methodType(int.class);
MethodHandle mh = MethodHandles.lookup().findVirtual(vcls, "getVersion", mt);
assertEquals(expected, (int) mh.invoke(vcls.newInstance()));
}
/*
* jdk.util.jar.enableMultiRelease=force should affect a custom class loader
*/
@Test
public void testClassLoader() throws Throwable {
try (JarFile jf = new JarFile(multirelease)) { // do not set runtime versioning
ClassLoader cldr = new CustomClassLoader(jf);
Class<?> vcls = cldr.loadClass("version.Version");
if (rtVersion == 9) {
try {
cldr.loadClass("version.PackagePrivate");
} catch (ClassNotFoundException x) {
if (force) throw x;
}
}
invokeMethod(vcls, force ? rtVersion : BASE_VERSION);
}
}
private static class CustomClassLoader extends ClassLoader {
private final JarFile jf;
CustomClassLoader(JarFile jf) throws Exception {
super(null);
this.jf = jf;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] b;
String entryName = name.replace(".", "/") + ".class";
JarEntry je = jf.getJarEntry(entryName);
if (je != null) {
try (InputStream is = jf.getInputStream(je)) {
b = new byte[(int) je.getSize()];
is.read(b);
}
return defineClass(name, b, 0, b.length);
}
throw new ClassNotFoundException(name);
} catch (IOException x) {
throw new ClassNotFoundException(x.getMessage());
}
}
}
@Test
public void testGetResourceAsStream() throws Exception {
String resource = rtVersion == 9 ? "/version/PackagePrivate.java" : "/version/Version.java";
// use fileRootClass as a base for getting resources
getResourceAsStream(rootClass, resource);
}
protected void getResourceAsStream(Class<?> rootClass, String resource) throws Exception {
try (InputStream is = rootClass.getResourceAsStream(resource)) {
byte[] bytes = is.readAllBytes();
resource = new String(bytes);
}
String match = "return " + rtVersion + ";";
assertTrue(resource.contains(match));
}
@Test
public void testGetResource() throws Exception {
String resource = rtVersion == 9 ? "/version/PackagePrivate.java" : "/version/Version.java";
// use rootClass as a base for getting resources
getResource(rootClass, resource);
}
protected void getResource(Class<?> rootClass, String resource) throws Exception {
URL url = rootClass.getResource(resource);
try (InputStream is = url.openStream()) {
byte[] bytes = is.readAllBytes();
resource = new String(bytes);
}
String match = "return " + rtVersion + ";";
assertTrue(resource.contains(match));
}
}

View file

@ -0,0 +1,112 @@
/*
* Copyright (c) 2015, 2026, 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 8132734 8144062
* @summary Test potential security related issues
* @library /lib/testlibrary/java/util/jar /test/lib/
* @build CreateMultiReleaseTestJars
* jdk.test.lib.compiler.Compiler
* jdk.test.lib.util.JarBuilder
* @run junit MultiReleaseJarSecurity
*/
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.security.CodeSigner;
import java.security.cert.Certificate;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipFile;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class MultiReleaseJarSecurity {
static final int MAJOR_VERSION = Runtime.version().major();
static final String USER_DIR = System.getProperty("user.dir", ".");
static final File MULTI_RELEASE = new File(USER_DIR, "multi-release.jar");
static final File SIGNED_MULTI_RELEASE = new File(USER_DIR, "signed-multi-release.jar");
@BeforeAll
public static void initialize() throws Exception {
CreateMultiReleaseTestJars creator = new CreateMultiReleaseTestJars();
creator.compileEntries();
creator.buildMultiReleaseJar();
creator.buildSignedMultiReleaseJar();
}
@AfterAll
public static void close() throws IOException {
Files.delete(MULTI_RELEASE.toPath());
Files.delete(SIGNED_MULTI_RELEASE.toPath());
}
@Test
public void testCertsAndSigners() throws IOException {
try (JarFile jf = new JarFile(SIGNED_MULTI_RELEASE, true, ZipFile.OPEN_READ, Runtime.version())) {
CertsAndSigners vcas = new CertsAndSigners(jf, jf.getJarEntry("version/Version.class"));
CertsAndSigners rcas = new CertsAndSigners(jf, jf.getJarEntry("META-INF/versions/" + MAJOR_VERSION + "/version/Version.class"));
assertArrayEquals(rcas.getCertificates(), vcas.getCertificates());
assertArrayEquals(rcas.getCodeSigners(), vcas.getCodeSigners());
}
}
private static class CertsAndSigners {
final private JarFile jf;
final private JarEntry je;
private boolean readComplete;
CertsAndSigners(JarFile jf, JarEntry je) {
this.jf = jf;
this.je = je;
}
Certificate[] getCertificates() throws IOException {
readEntry();
return je.getCertificates();
}
CodeSigner[] getCodeSigners() throws IOException {
readEntry();
return je.getCodeSigners();
}
private void readEntry() throws IOException {
if (!readComplete) {
try (InputStream is = jf.getInputStream(je)) {
is.readAllBytes();
}
readComplete = true;
}
}
}
}

View file

@ -0,0 +1,3 @@
modules = \
jdk.compiler \
jdk.jartool

View file

@ -0,0 +1,244 @@
/*
* Copyright (c) 2016, 2026, 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 8163798 8189611 8211728
* @summary basic tests for multi-release jar versioned streams
* @library /test/lib
* @build jdk.test.lib.Platform
* jdk.test.lib.util.FileUtils
* @run junit TestVersionedStream
*/
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.spi.ToolProvider;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipFile;
import jdk.test.lib.util.FileUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.*;
public class TestVersionedStream {
private static final Path userdir;
private static final Set<String> unversionedEntryNames;
private static final int LATEST_VERSION = Runtime.version().feature();
static {
userdir = Paths.get(System.getProperty("user.dir", "."));
// These are not real class files even though they end with .class.
// They are resource files so jar tool validation won't reject them.
// But they are what we want to test, especially q/Bar.class that
// could be in a concealed package if this was a modular multi-release
// jar.
createFiles(
"base/p/Bar.class",
"base/p/Foo.class",
"base/p/Main.class",
"v9/p/Foo.class",
"v10/p/Foo.class",
"v10/q/Bar.class",
"v" + LATEST_VERSION + "/p/Bar.class",
"v" + LATEST_VERSION + "/p/Foo.class",
"v" + LATEST_VERSION + "/META-INF/Foo.class"
);
jar("cf mmr.jar -C base . " +
"--release 9 -C v9 . " +
"--release 10 -C v10 . " +
"--release " + LATEST_VERSION + " -C v" + LATEST_VERSION + " .");
System.out.println("Contents of mmr.jar\n=======");
try (JarFile jf = new JarFile("mmr.jar")) {
unversionedEntryNames = jf.stream()
.map(je -> je.getName())
.peek(System.out::println)
.map(nm -> nm.startsWith("META-INF/versions/")
? nm.replaceFirst("META-INF/versions/\\d+/", "")
: nm)
.collect(Collectors.toCollection(LinkedHashSet::new));
} catch (IOException e) {
throw new RuntimeException("Failed to init \"unversionedEntryNames\"", e);
}
System.out.println("=======");
}
@AfterAll
public static void close() throws IOException {
Files.walk(userdir, 1)
.filter(p -> !p.equals(userdir))
.forEach(p -> {
try {
if (Files.isDirectory(p)) {
FileUtils.deleteFileTreeWithRetry(p);
} else {
FileUtils.deleteFileIfExistsWithRetry(p);
}
} catch (IOException x) {
throw new UncheckedIOException(x);
}
});
}
public static Stream<Runtime.Version> arguments() {
return Stream.of(
Runtime.Version.parse("8"),
Runtime.Version.parse("9"),
Runtime.Version.parse("10"),
Runtime.Version.parse(Integer.toString(LATEST_VERSION)),
JarFile.baseVersion(),
JarFile.runtimeVersion()
);
}
@ParameterizedTest
@MethodSource("arguments")
public void versionTest(Runtime.Version version) throws Exception {
try (JarFile jf = new JarFile(new File("mmr.jar"), false, ZipFile.OPEN_READ, version);
Stream<JarEntry> jes = jf.versionedStream())
{
assertNotNull(jes);
// put versioned entries in list so we can reuse them
List<JarEntry> versionedEntries = jes.collect(Collectors.toList());
assertTrue(versionedEntries.size() > 0);
// also keep the names
List<String> versionedNames = versionedEntries.stream()
.map(JarEntry::getName)
.collect(Collectors.toList());
// verify the correct order while building enames
List<String> unversionedOrder = new ArrayList<>(unversionedEntryNames);
unversionedOrder.retainAll(versionedNames);
assertIterableEquals(unversionedOrder, versionedNames,
"versioned entries not in same order as unversioned entries");
// verify the contents:
// value.[0] end of the path
// value.[1] versioned path/real name
Map<String,String[]> expected = new HashMap<>();
expected.put("p/Bar.class", new String[] { "base/p/Bar.class", "p/Bar.class" });
expected.put("p/Main.class", new String[] { "base/p/Main.class", "p/Main.class" });
int majorVersion = version.major();
switch (majorVersion) {
case 8:
expected.put("p/Foo.class", new String[]
{ "base/p/Foo.class", "p/Foo.class" });
break;
case 9:
expected.put("p/Foo.class", new String[]
{ "v9/p/Foo.class", "META-INF/versions/9/p/Foo.class" });
break;
case 10:
expected.put("p/Foo.class", new String[]
{ "v10/p/Foo.class", "META-INF/versions/10/p/Foo.class" });
expected.put("q/Bar.class", new String[]
{ "v10/q/Bar.class", "META-INF/versions/10/q/Bar.class" });
break;
default:
if (majorVersion == LATEST_VERSION) {
expected.put("p/Bar.class",
new String[] { "v" + LATEST_VERSION + "/p/Bar.class",
"META-INF/versions/" + LATEST_VERSION + "/p/Bar.class"});
expected.put("p/Foo.class",
new String[]{ "v" + LATEST_VERSION + "/p/Foo.class",
"META-INF/versions/" + LATEST_VERSION + "/p/Foo.class"});
expected.put("q/Bar.class",
new String[] { "q/Bar.class", "META-INF/versions/10/q/Bar.class"});
} else {
fail("Test out of date, please add more cases");
}
}
expected.entrySet().stream().forEach(e -> {
String name = e.getKey();
int i = versionedNames.indexOf(name);
assertTrue(i != -1, name + " not in enames");
JarEntry je = versionedEntries.get(i);
try (InputStream is = jf.getInputStream(je)) {
String s = new String(is.readAllBytes()).replaceAll(System.lineSeparator(), "");
// end of the path
assertTrue(s.endsWith(e.getValue()[0]), s);
// getRealName()
assertTrue(je.getRealName().equals(e.getValue()[1]));
} catch (IOException x) {
throw new UncheckedIOException(x);
}
});
if (!unversionedEntryNames.contains("META-INF/Foo.class") ||
versionedNames.indexOf("META-INF/Foo.class") != -1) {
fail("versioned META-INF/Foo.class test failed");
}
}
}
private static void createFiles(String... files) {
ArrayList<String> list = new ArrayList();
Arrays.stream(files)
.map(f -> Paths.get(userdir.toAbsolutePath().toString(), f))
.forEach(p -> {
try {
Files.createDirectories(p.getParent());
Files.createFile(p);
list.clear();
list.add(p.toString().replace(File.separatorChar, '/'));
Files.write(p, list);
} catch (IOException x) {
throw new UncheckedIOException(x);
}});
}
private static void jar(String args) {
ToolProvider jar = ToolProvider.findFirst("jar").orElseThrow();
jar.run(System.out, System.err, args.split(" +"));
}
}

Binary file not shown.

Binary file not shown.