This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 8a15ec654f9150666a11a4352fb88f02f5104181 Author: opencode <[email protected]> AuthorDate: Fri Oct 16 13:43:25 2026 +0200 Resolve nested JAR entries in JarFileUrlNestedJar using the JarURLConnection The constructor previously derived the entry name for the nested JAR by manually extracting the text after the "!/" separator from the URL string. Percent-encoding in the entry name is not visible in the URL string in decoded form, so for URLs that named the entry using percent encoding (for example a nested JAR whose file name contains a space), the manual lookup did not find the entry the JDK itself resolves the URL to. The Jar was then constructed with no entry, and the problem only surfaced later as a NullPointerException when the content of the Jar was first accessed. The entry is now resolved with JarURLConnection.getJarEntry(), which applies the same decoding the JDK applies when it resolves the URL. This is the same approach JarFileUrlJar already uses. If the URL does not name an entry at all (it ends with "!/"), a FileNotFoundException is now thrown when the Jar is constructed rather than leaving the Jar in a state that fails unexpectedly later. Add TestJarFileUrlNestedJar to cover plain and percent-encoded entry names as well as the missing entry, empty entry name and missing separator cases. --- .../tomcat/util/scan/JarFileUrlNestedJar.java | 12 +- .../tomcat/util/scan/TestJarFileUrlNestedJar.java | 164 +++++++++++++++++++++ 2 files changed, 172 insertions(+), 4 deletions(-) diff --git a/java/org/apache/tomcat/util/scan/JarFileUrlNestedJar.java b/java/org/apache/tomcat/util/scan/JarFileUrlNestedJar.java index a189aa843a..c0583b5897 100644 --- a/java/org/apache/tomcat/util/scan/JarFileUrlNestedJar.java +++ b/java/org/apache/tomcat/util/scan/JarFileUrlNestedJar.java @@ -16,6 +16,7 @@ */ package org.apache.tomcat.util.scan; +import java.io.FileNotFoundException; import java.io.IOException; import java.net.JarURLConnection; import java.net.URL; @@ -43,10 +44,13 @@ public class JarFileUrlNestedJar extends AbstractInputStreamJar { jarConn.setUseCaches(false); warFile = jarConn.getJarFile(); - String urlAsString = url.toString(); - int pathStart = urlAsString.indexOf("!/") + 2; - String jarPath = urlAsString.substring(pathStart); - jarEntry = warFile.getJarEntry(jarPath); + // Use the connection to resolve the entry rather than parsing the URL string manually so that any + // percent-encoding in the entry name is handled the same way as the JDK handles it. A missing entry + // causes jarConn.getJarFile() above to throw. + jarEntry = jarConn.getJarEntry(); + if (jarEntry == null) { + throw new FileNotFoundException(url.toExternalForm()); + } } diff --git a/test/org/apache/tomcat/util/scan/TestJarFileUrlNestedJar.java b/test/org/apache/tomcat/util/scan/TestJarFileUrlNestedJar.java new file mode 100644 index 0000000000..0eceb7614a --- /dev/null +++ b/test/org/apache/tomcat/util/scan/TestJarFileUrlNestedJar.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.tomcat.util.scan; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.Jar; + +public class TestJarFileUrlNestedJar { + + private static final String TEST_ENTRY = "Test.class"; + private static final byte[] TEST_CONTENT = "test content".getBytes(StandardCharsets.UTF_8); + + /* + * Creates a WAR that contains a nested JAR at the given path. The nested JAR contains a single entry named + * TEST_ENTRY with the content TEST_CONTENT. + */ + private File createWar(String nestedJarPath) throws IOException { + File file = File.createTempFile("nested-", ".war"); + file.deleteOnExit(); + + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + + try (FileOutputStream fos = new FileOutputStream(file); + JarOutputStream war = new JarOutputStream(fos, manifest)) { + + war.putNextEntry(new JarEntry(nestedJarPath)); + war.write(createNestedJar()); + war.closeEntry(); + } + + return file; + } + + + private static byte[] createNestedJar() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + + try (JarOutputStream jar = new JarOutputStream(baos, manifest)) { + jar.putNextEntry(new JarEntry(TEST_ENTRY)); + jar.write(TEST_CONTENT); + jar.closeEntry(); + } + + return baos.toByteArray(); + } + + + private static byte[] readJar(Jar jar) throws IOException { + try (InputStream is = jar.getInputStream(TEST_ENTRY)) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] data = new byte[1024]; + int len; + while ((len = is.read(data)) > 0) { + buffer.write(data, 0, len); + } + return buffer.toByteArray(); + } + } + + + /* + * A nested JAR referenced with a plain entry name must be readable. + */ + @Test + public void testPlainEntryName() throws Exception { + String nestedPath = "WEB-INF/lib/nested.jar"; + File war = createWar(nestedPath); + + try (Jar jar = new JarFileUrlNestedJar(new URL("jar:file:" + war.getAbsolutePath() + "!/" + nestedPath))) { + Assert.assertTrue(jar.exists(TEST_ENTRY)); + Assert.assertArrayEquals(TEST_CONTENT, readJar(jar)); + } + } + + + /* + * A percent-encoded entry name must resolve to the same entry that the JDK resolves it to, rather than + * resulting in a JAR with no usable content. + */ + @Test + public void testEncodedEntryName() throws Exception { + File war = createWar("WEB-INF/lib/my jar.jar"); + + try (Jar jar = new JarFileUrlNestedJar( + new URL("jar:file:" + war.getAbsolutePath() + "!/WEB-INF/lib/my%20jar.jar"))) { + Assert.assertTrue(jar.exists(TEST_ENTRY)); + Assert.assertArrayEquals(TEST_CONTENT, readJar(jar)); + } + } + + + /* + * A URL that does not refer to an existing entry must fail when the JAR is constructed rather than later when + * the content is accessed. + */ + @Test + public void testMissingEntryThrows() throws Exception { + File war = createWar("WEB-INF/lib/nested.jar"); + + Assert.assertThrows(IOException.class, () -> new JarFileUrlNestedJar( + new URL("jar:file:" + war.getAbsolutePath() + "!/WEB-INF/lib/missing.jar"))); + } + + + /* + * A URL that does not name an entry (it refers to the whole archive) must fail when the JAR is constructed + * rather than later when the content is accessed. JarFactory routes such URLs elsewhere. This covers direct + * construction of JarFileUrlNestedJar with such a URL. + */ + @Test + public void testEmptyEntryNameThrows() throws Exception { + File war = createWar("WEB-INF/lib/nested.jar"); + + Assert.assertThrows(FileNotFoundException.class, + () -> new JarFileUrlNestedJar(new URL("jar:file:" + war.getAbsolutePath() + "!/"))); + } + + + /* + * A URL that does not contain the JAR entry separator must fail with an IOException before the JAR can be used. + * In practice the JDK's jar URL handler rejects such a spec ("no !/ in spec") with a MalformedURLException when + * the URL is created, so a URL of this form never reaches JarFileUrlNestedJar via JarFactory. + */ + @Test + public void testMissingSeparatorThrows() throws Exception { + File war = createWar("WEB-INF/lib/nested.jar"); + + Assert.assertThrows(IOException.class, + () -> new JarFileUrlNestedJar(new URL("jar:file:" + war.getAbsolutePath() + "!x"))); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
