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]

Reply via email to