From 4af9ed4d1d41653a89ee3a7d40db6e27d9ffd330 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 26 Oct 2023 18:58:15 -0700 Subject: [PATCH] Fix Tomcat TldScanner issues by returning raw zip data for nested jars Update JarUrlConnection so that the full raw zip data is returned from nested jars when no entry name is specified. This update allows Tomcat's `WarURLConnection` to work with our nested connections since they can parse the returned raw zip data. Fixes gh-38047 --- .../boot/loader/jar/NestedJarFile.java | 31 +++++++++++++++++++ .../net/protocol/jar/JarUrlConnection.java | 13 ++++++-- .../net/protocol/jar/UrlJarFileFactory.java | 8 ++--- .../protocol/jar/JarUrlConnectionTests.java | 17 ++++++++-- 4 files changed, 60 insertions(+), 9 deletions(-) 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"));