This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 88624343b6f5d6116d06fc44c96ada0901d3590b Author: opencode <[email protected]> AuthorDate: Mon Oct 12 21:06:27 2026 +0200 Keep entries below an invalid multi-release version directory accessible processArchivesEntriesForMultiRelease() parsed the name of the directory below META-INF/versions/ that precedes every entry of a multi-release JAR nested in a packed WAR without checking that it is a version number. A JAR with an entry below a directory such as 9x, or directly below META-INF/versions/, caused a NumberFormatException that escaped getArchiveEntries(), which only handles IOException, so the first read of the archive index failed for that resource set. Treat an entry that is not below a valid version directory as a regular entry, which is what the multi-release support in the JDK does. It stays in the index under its full name, the only name such an entry is ever requested under, and the bloom filter built from the index already handles those names the same way. Entries below a valid version directory are processed as before. Add a unit test for a packed WAR with a multi-release JAR that contains both kinds of entry. --- .../catalina/webresources/JarWarResourceSet.java | 18 ++++- .../webresources/TestJarWarResourceSet.java | 80 ++++++++++++++++++++++ 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/java/org/apache/catalina/webresources/JarWarResourceSet.java b/java/org/apache/catalina/webresources/JarWarResourceSet.java index 69506987f7..9896ca2752 100644 --- a/java/org/apache/catalina/webresources/JarWarResourceSet.java +++ b/java/org/apache/catalina/webresources/JarWarResourceSet.java @@ -190,10 +190,12 @@ public class JarWarResourceSet extends AbstractArchiveResourceSet { int targetVersion = Runtime.version().feature(); Map<String,VersionedJarEntry> versionedEntries = new HashMap<>(); + Map<String,JarEntry> otherEntries = new HashMap<>(); Iterator<Entry<String,JarEntry>> iter = archiveEntries.entrySet().iterator(); while (iter.hasNext()) { Entry<String,JarEntry> entry = iter.next(); String name = entry.getKey(); + JarEntry jarEntry = entry.getValue(); if (name.startsWith("META-INF/versions/")) { // Remove the multi-release version iter.remove(); @@ -202,7 +204,16 @@ public class JarWarResourceSet extends AbstractArchiveResourceSet { int i = name.indexOf('/', 18); if (i > 0) { String baseName = name.substring(i + 1); - int version = Integer.parseInt(name.substring(18, i)); + int version; + try { + version = Integer.parseInt(name.substring(18, i)); + } catch (NumberFormatException e) { + // The entry is not below a valid version directory, so + // it is not a versioned entry and remains available + // under its full name + otherEntries.put(name, jarEntry); + continue; + } // Ignore any entries targeting for a later version than // the target for this runtime @@ -211,20 +222,21 @@ public class JarWarResourceSet extends AbstractArchiveResourceSet { if (versionedJarEntry == null) { // No versioned entry found for this name. Create // one. - versionedEntries.put(baseName, new VersionedJarEntry(version, entry.getValue())); + versionedEntries.put(baseName, new VersionedJarEntry(version, jarEntry)); } else { // Ignore any entry for which we have already found // a later version if (version > versionedJarEntry.version()) { // Replace the entry targeted at an earlier // version - versionedEntries.put(baseName, new VersionedJarEntry(version, entry.getValue())); + versionedEntries.put(baseName, new VersionedJarEntry(version, jarEntry)); } } } } } } + archiveEntries.putAll(otherEntries); for (Entry<String,VersionedJarEntry> versionedJarEntry : versionedEntries.entrySet()) { archiveEntries.put(versionedJarEntry.getKey(), versionedJarEntry.getValue().jarEntry()); diff --git a/test/org/apache/catalina/webresources/TestJarWarResourceSet.java b/test/org/apache/catalina/webresources/TestJarWarResourceSet.java index be4ec953c4..89dee0828c 100644 --- a/test/org/apache/catalina/webresources/TestJarWarResourceSet.java +++ b/test/org/apache/catalina/webresources/TestJarWarResourceSet.java @@ -16,9 +16,18 @@ */ package org.apache.catalina.webresources; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Map; +import java.util.jar.Attributes; import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import org.junit.Assert; import org.junit.Before; @@ -79,4 +88,75 @@ public class TestJarWarResourceSet extends TomcatBaseTest { resourceSet.gc(); } + + @Test + public void testMultiReleaseWithInvalidVersionDirectories() throws Exception { + // An entry below a directory whose name is not a valid version number + // must not stop the archive index being built. Such an entry is not a + // versioned entry, so it stays in the index under its full name. An + // entry below a valid version directory is indexed under its base + // name. + File warFile = File.createTempFile("test-jar-war-resource-set", ".war"); + warFile.deleteOnExit(); + try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(warFile))) { + out.putNextEntry(new ZipEntry("WEB-INF/lib/test.jar")); + out.write(createMultiReleaseJar()); + out.closeEntry(); + } + + StandardRoot resourceRoot = new StandardRoot(); + resourceRoot.setArchiveIndexStrategy("BLOOM"); + JarWarResourceSet resourceSet = new JarWarResourceSet(resourceRoot, "/WEB-INF/classes", + warFile.getAbsolutePath(), "WEB-INF/lib/test.jar", "/"); + + Map<String,JarEntry> entries = resourceSet.getArchiveEntries(true); + + Assert.assertTrue(entries.containsKey("META-INF/versions/9x/foo.txt")); + Assert.assertTrue(entries.containsKey("META-INF/versions//bar.txt")); + Assert.assertTrue(entries.containsKey("META-INF/versions/99999999999999/ovf.txt")); + Assert.assertTrue(entries.containsKey("ok.txt")); + Assert.assertEquals("META-INF/versions/9/ok.txt", entries.get("ok.txt").getName()); + Assert.assertFalse(entries.containsKey("META-INF/versions/9/ok.txt")); + Assert.assertTrue(entries.containsKey("real.txt")); + + // The bloom filter is built from the resulting index + JarContents jarContents = resourceSet.jarContents; + Assert.assertNotNull(jarContents); + Assert.assertTrue(jarContents.mightContainResource("META-INF/versions/9x/foo.txt", "")); + Assert.assertTrue(jarContents.mightContainResource("ok.txt", "")); + Assert.assertTrue(jarContents.mightContainResource("real.txt", "")); + + // The index and the bloom filter have to agree on the name an entry is + // stored under, otherwise these lookups would be rejected by the filter + WebResource invalidVersion = + resourceSet.getResource("/WEB-INF/classes/META-INF/versions/9x/foo.txt"); + Assert.assertTrue(invalidVersion instanceof JarWarResource); + Assert.assertTrue(invalidVersion.exists()); + + WebResource versioned = resourceSet.getResource("/WEB-INF/classes/ok.txt"); + Assert.assertTrue(versioned instanceof JarWarResource); + Assert.assertTrue(versioned.exists()); + + resourceSet.gc(); + } + + private static byte[] createMultiReleaseJar() throws IOException { + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().putValue("Multi-Release", "true"); + + String[] names = { "META-INF/versions/9/ok.txt", "META-INF/versions/9x/foo.txt", + "META-INF/versions//bar.txt", "META-INF/versions/99999999999999/ovf.txt", "real.txt" }; + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (JarOutputStream jos = new JarOutputStream(baos, manifest)) { + for (String name : names) { + jos.putNextEntry(new JarEntry(name)); + jos.write("x".getBytes(StandardCharsets.UTF_8)); + jos.closeEntry(); + } + } + + return baos.toByteArray(); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
