Merge branch '3.2.x' into 3.3.x

This commit is contained in:
Andy Wilkinson
2024-08-22 13:07:24 +01:00
10 changed files with 340 additions and 45 deletions

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.jar;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.zip.Inflater;
import java.util.zip.ZipEntry;
/**
* Helper class to iterate entries in a jar file and check that content matches a related
* entry.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
class JarEntriesStream implements Closeable {
private static final int BUFFER_SIZE = 4 * 1024;
private final JarInputStream in;
private final byte[] inBuffer = new byte[BUFFER_SIZE];
private final byte[] compareBuffer = new byte[BUFFER_SIZE];
private final Inflater inflater = new Inflater(true);
private JarEntry entry;
JarEntriesStream(InputStream in) throws IOException {
this.in = new JarInputStream(in);
}
JarEntry getNextEntry() throws IOException {
this.entry = this.in.getNextJarEntry();
if (this.entry != null) {
this.entry.getSize();
}
this.inflater.reset();
return this.entry;
}
boolean matches(boolean directory, int size, int compressionMethod, InputStreamSupplier streamSupplier)
throws IOException {
if (this.entry.isDirectory() != directory) {
fail("directory");
}
if (this.entry.getMethod() != compressionMethod) {
fail("compression method");
}
if (this.entry.isDirectory()) {
this.in.closeEntry();
return true;
}
try (DataInputStream expected = new DataInputStream(getInputStream(size, streamSupplier))) {
assertSameContent(expected);
}
return true;
}
private InputStream getInputStream(int size, InputStreamSupplier streamSupplier) throws IOException {
InputStream inputStream = streamSupplier.get();
return (this.entry.getMethod() != ZipEntry.DEFLATED) ? inputStream
: new ZipInflaterInputStream(inputStream, this.inflater, size);
}
private void assertSameContent(DataInputStream expected) throws IOException {
int len;
while ((len = this.in.read(this.inBuffer)) > 0) {
try {
expected.readFully(this.compareBuffer, 0, len);
if (Arrays.equals(this.inBuffer, 0, len, this.compareBuffer, 0, len)) {
continue;
}
}
catch (EOFException ex) {
// Continue and throw exception due to mismatched content length.
}
fail("content");
}
if (expected.read() != -1) {
fail("content");
}
}
private void fail(String check) {
throw new IllegalStateException("Content mismatch when reading security info for entry '%s' (%s check)"
.formatted(this.entry.getName(), check));
}
@Override
public void close() throws IOException {
this.inflater.end();
this.in.close();
}
@FunctionalInterface
interface InputStreamSupplier {
InputStream get() throws IOException;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -81,30 +81,31 @@ final class SecurityInfo {
* @return the security info
* @throws IOException on I/O error
*/
@SuppressWarnings("resource")
private static SecurityInfo load(ZipContent content) throws IOException {
int size = content.size();
boolean hasSecurityInfo = false;
Certificate[][] entryCertificates = new Certificate[size][];
CodeSigner[][] entryCodeSigners = new CodeSigner[size][];
try (JarInputStream in = new JarInputStream(content.openRawZipData().asInputStream())) {
JarEntry jarEntry = in.getNextJarEntry();
while (jarEntry != null) {
in.closeEntry(); // Close to trigger a read and set certs/signers
Certificate[] certificates = jarEntry.getCertificates();
CodeSigner[] codeSigners = jarEntry.getCodeSigners();
if (certificates != null || codeSigners != null) {
ZipContent.Entry contentEntry = content.getEntry(jarEntry.getName());
if (contentEntry != null) {
try (JarEntriesStream entries = new JarEntriesStream(content.openRawZipData().asInputStream())) {
JarEntry entry = entries.getNextEntry();
while (entry != null) {
ZipContent.Entry relatedEntry = content.getEntry(entry.getName());
if (relatedEntry != null && entries.matches(relatedEntry.isDirectory(),
relatedEntry.getUncompressedSize(), relatedEntry.getCompressionMethod(),
() -> relatedEntry.openContent().asInputStream())) {
Certificate[] certificates = entry.getCertificates();
CodeSigner[] codeSigners = entry.getCodeSigners();
if (certificates != null || codeSigners != null) {
hasSecurityInfo = true;
entryCertificates[contentEntry.getLookupIndex()] = certificates;
entryCodeSigners[contentEntry.getLookupIndex()] = codeSigners;
entryCertificates[relatedEntry.getLookupIndex()] = certificates;
entryCodeSigners[relatedEntry.getLookupIndex()] = codeSigners;
}
}
jarEntry = in.getNextJarEntry();
entry = entries.getNextEntry();
}
return (!hasSecurityInfo) ? NONE : new SecurityInfo(entryCertificates, entryCodeSigners);
}
return (!hasSecurityInfo) ? NONE : new SecurityInfo(entryCertificates, entryCodeSigners);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@ import java.util.zip.InflaterInputStream;
*
* @author Phillip Webb
*/
abstract class ZipInflaterInputStream extends InflaterInputStream {
class ZipInflaterInputStream extends InflaterInputStream {
private int available;

View File

@@ -412,6 +412,25 @@ class NestedJarFileTests {
assertThat(nested).isEqualTo(jdk);
}
@Test
void mismatchedStreamEntriesThrowsException() throws IOException {
File mismatchJar = new File("src/test/resources/jars/mismatch.jar");
IllegalStateException failure = null;
try (NestedJarFile innerJar = new NestedJarFile(mismatchJar, "inner.jar")) {
Enumeration<JarEntry> entries = innerJar.entries();
while (entries.hasMoreElements()) {
try {
entries.nextElement().getCodeSigners();
}
catch (IllegalStateException ex) {
failure = (failure != null) ? failure : ex;
}
}
}
assertThat(failure)
.hasMessage("Content mismatch when reading security info for entry 'content' (content check)");
}
private List<String> collectComments(JarFile jarFile) throws IOException {
try (jarFile) {
List<String> comments = new ArrayList<>();