diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java index d711b9137a..bddd274e22 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java @@ -17,6 +17,7 @@ package org.springframework.boot.loader.jar; import java.io.File; +import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -143,6 +144,13 @@ public class NestedJarFile extends JarFile { this.version = (version != null) ? version.feature() : baseVersion().feature(); } + public InputStream getRawZipDataInputStream() throws IOException { + RawZipDataInputStream inputStream = new RawZipDataInputStream( + this.resources.zipContent().openRawZipData().asInputStream()); + this.resources.addInputStream(inputStream); + return inputStream; + } + @Override public Manifest getManifest() throws IOException { try { @@ -799,4 +807,27 @@ public class NestedJarFile extends JarFile { } + /** + * {@link InputStream} for raw zip data. + */ + private class RawZipDataInputStream extends FilterInputStream { + + private volatile boolean closed; + + protected RawZipDataInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + if (this.closed) { + return; + } + this.closed = true; + super.close(); + NestedJarFile.this.resources.removeInputStream(this); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java index dc51bfe4eb..c9a7475a1f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java @@ -175,11 +175,12 @@ final class JarUrlConnection extends java.net.JarURLConnection { if (this.notFound != null) { throwFileNotFound(); } - if (this.entryName == null) { + URL jarFileURL = getJarFileURL(); + if (this.entryName == null && !UrlJarFileFactory.isNestedUrl(jarFileURL)) { throw new IOException("no entry name specified"); } - if (!getUseCaches() && Optimizations.isEnabled(false)) { - JarFile cached = jarFiles.getCached(getJarFileURL()); + if (!getUseCaches() && Optimizations.isEnabled(false) && this.entryName != null) { + JarFile cached = jarFiles.getCached(jarFileURL); if (cached != null) { if (cached.getEntry(this.entryName) != null) { return emptyInputStream; @@ -188,6 +189,12 @@ final class JarUrlConnection extends java.net.JarURLConnection { } connect(); if (this.jarEntry == null) { + if (this.jarFile instanceof NestedJarFile nestedJarFile) { + // In order to work with Tomcat's TLD scanning and WarURLConnection we + // return the raw zip data rather than failing because there is no entry. + // See gh-38047 for details. + return nestedJarFile.getRawZipDataInputStream(); + } throwFileNotFound(); } return new ConnectionInputStream(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java index 35ba9283ca..b35bc2435c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java @@ -80,10 +80,6 @@ class UrlJarFileFactory { return new UrlJarFile(new File(path), version, closeAction); } - private boolean isNestedUrl(URL url) { - return url.getProtocol().equalsIgnoreCase("nested"); - } - private JarFile createJarFileForNested(URL url, Runtime.Version version, Consumer closeAction) throws IOException { NestedLocation location = NestedLocation.fromUrl(url); @@ -120,4 +116,8 @@ class UrlJarFileFactory { } } + static boolean isNestedUrl(URL url) { + return url.getProtocol().equalsIgnoreCase("nested"); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java index 5d7ccf616b..d2445b48e9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java @@ -22,6 +22,7 @@ import java.io.FileOutputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.net.URL; import java.net.URLConnection; import java.nio.charset.StandardCharsets; @@ -237,8 +238,8 @@ class JarUrlConnectionTests { } @Test - void getInputStreamWhenHasNoEntryThrowsException() throws Exception { - JarUrlConnection connection = JarUrlConnection.open(this.url); + void getInputStreamWhenNotNestedAndHasNoEntryThrowsException() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file)); assertThatIOException().isThrownBy(() -> connection.getInputStream()).withMessage("no entry name specified"); } @@ -271,6 +272,18 @@ class JarUrlConnectionTests { .withMessageContaining("JAR entry missing.dat not found in"); } + @Test // gh-38047 + void getInputStreamWhenNoEntryAndNestedReturnsFullJarInputStream() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar")); + File outFile = new File(this.temp, "out.zip"); + try (OutputStream out = new FileOutputStream(outFile)) { + connection.getInputStream().transferTo(out); + } + try (JarFile outJar = new JarFile(outFile)) { + assertThat(outJar.getEntry("3.dat")).isNotNull(); + } + } + @Test void getInputStreamReturnsInputStream() throws IOException { JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "3.dat"));