This is an automated email from the ASF dual-hosted git repository.
markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push:
new ace667fdb2 Review "Avoid exceptions when indexing malformed archive
entry names"
ace667fdb2 is described below
commit ace667fdb26c13fe0539580ed03be953a5a720e5
Author: Mark Thomas <[email protected]>
AuthorDate: Fri Oct 30 17:44:43 2026 +0000
Review "Avoid exceptions when indexing malformed archive entry names"
Fix various inconsistencies in the multi-release JAR handling. Handling
should now be consistent within Tomcat and aligned with the JRE.
Co-authored by: Claude / Sonnet 5.5
---
.../apache/catalina/webresources/JarContents.java | 20 +-
.../catalina/webresources/JarWarResourceSet.java | 55 +++--
.../tomcat/util/scan/AbstractInputStreamJar.java | 77 +++----
.../org/apache/tomcat/util/scan/JarFileUrlJar.java | 70 ++++---
.../apache/tomcat/util/scan/MultiReleaseUtil.java | 97 +++++++++
.../catalina/webresources/TestJarContents.java | 65 +++++-
.../webresources/TestJarWarResourceSet.java | 88 +++++++-
.../util/scan/TestAbstractInputStreamJar.java | 222 +++++++++++++++++++++
.../apache/tomcat/util/scan/TestJarFileUrlJar.java | 198 +++++++++++++++++-
.../tomcat/util/scan/TestMultiVersionUtil.java | 134 +++++++++++++
webapps/docs/changelog.xml | 7 +
11 files changed, 898 insertions(+), 135 deletions(-)
diff --git a/java/org/apache/catalina/webresources/JarContents.java
b/java/org/apache/catalina/webresources/JarContents.java
index abb13c93b0..f8954c3b2e 100644
--- a/java/org/apache/catalina/webresources/JarContents.java
+++ b/java/org/apache/catalina/webresources/JarContents.java
@@ -22,6 +22,9 @@ import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
+import org.apache.tomcat.util.scan.MultiReleaseUtil;
+import org.apache.tomcat.util.scan.MultiReleaseUtil.MultiVersionInfo;
+
/**
* This class represents the contents of a jar by determining whether a given
resource <b>might</b> be in the cache,
* based on a bloom filter. This is not a general-purpose bloom filter because
it contains logic to strip out characters
@@ -95,19 +98,10 @@ public final class JarContents {
}
// Versioned entries should be added to the table according to their
real name
- if (name.startsWith("META-INF/versions/", startPos)) {
- int i = name.indexOf('/', 18 + startPos);
- if (i > 0) {
- try {
- int version = Integer.parseInt(name.substring(18 +
startPos, i));
- if (version <= Runtime.version().feature()) {
- startPos = i + 1;
- }
- } catch (NumberFormatException e) {
- // The segment is not a valid version number, so the entry
- // is not a versioned entry that can be resolved under its
- // base name and is added to the table under its full name.
- }
+ MultiVersionInfo multiVersionInfo =
MultiReleaseUtil.parseVersion(name);
+ if (multiVersionInfo != null) {
+ if (multiVersionInfo.isValid() && multiVersionInfo.version() <=
Runtime.version().feature()) {
+ startPos = multiVersionInfo.namePos();
}
if (startPos == name.length()) {
return;
diff --git a/java/org/apache/catalina/webresources/JarWarResourceSet.java
b/java/org/apache/catalina/webresources/JarWarResourceSet.java
index 9896ca2752..2e7591c2e0 100644
--- a/java/org/apache/catalina/webresources/JarWarResourceSet.java
+++ b/java/org/apache/catalina/webresources/JarWarResourceSet.java
@@ -33,6 +33,8 @@ import org.apache.catalina.LifecycleException;
import org.apache.catalina.WebResource;
import org.apache.catalina.WebResourceRoot;
import org.apache.tomcat.util.buf.UriUtil;
+import org.apache.tomcat.util.scan.MultiReleaseUtil;
+import org.apache.tomcat.util.scan.MultiReleaseUtil.MultiVersionInfo;
/**
* Represents a {@link org.apache.catalina.WebResourceSet} based on a JAR file
that is nested inside a packed WAR file.
@@ -190,53 +192,42 @@ 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/")) {
+ MultiVersionInfo multiVersionInfo =
MultiReleaseUtil.parseVersion(name);
+ if (multiVersionInfo != null && multiVersionInfo.isValid()) {
// Remove the multi-release version
iter.remove();
- // Get the base name and version for this versioned entry
- int i = name.indexOf('/', 18);
- if (i > 0) {
- String baseName = name.substring(i + 1);
- 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);
+ // Ignore any entries targeting for a later version than
+ // the target for this runtime
+ if (multiVersionInfo.version() <= targetVersion) {
+ String baseName =
name.substring(multiVersionInfo.namePos());
+ if (baseName.isEmpty()) {
+ // The entry for the version directory itself. There
is no resource to expose.
continue;
}
-
- // Ignore any entries targeting for a later version than
- // the target for this runtime
- if (version <= targetVersion) {
- VersionedJarEntry versionedJarEntry =
versionedEntries.get(baseName);
- if (versionedJarEntry == null) {
- // No versioned entry found for this name. Create
- // one.
- 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, jarEntry));
- }
+ VersionedJarEntry versionedJarEntry =
versionedEntries.get(baseName);
+ if (versionedJarEntry == null) {
+ // No versioned entry found for this name. Create
+ // one.
+ versionedEntries.put(baseName, new
VersionedJarEntry(multiVersionInfo.version(), jarEntry));
+ } else {
+ // Ignore any entry for which we have already found
+ // a later version
+ if (multiVersionInfo.version() >
versionedJarEntry.version()) {
+ // Replace the entry targeted at an earlier
+ // version
+ versionedEntries.put(baseName,
+ new
VersionedJarEntry(multiVersionInfo.version(), jarEntry));
}
}
}
}
}
- archiveEntries.putAll(otherEntries);
for (Entry<String,VersionedJarEntry> versionedJarEntry :
versionedEntries.entrySet()) {
archiveEntries.put(versionedJarEntry.getKey(),
versionedJarEntry.getValue().jarEntry());
diff --git a/java/org/apache/tomcat/util/scan/AbstractInputStreamJar.java
b/java/org/apache/tomcat/util/scan/AbstractInputStreamJar.java
index cfd80056a3..c92252bcc3 100644
--- a/java/org/apache/tomcat/util/scan/AbstractInputStreamJar.java
+++ b/java/org/apache/tomcat/util/scan/AbstractInputStreamJar.java
@@ -21,11 +21,11 @@ import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
-import java.util.Map.Entry;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
import org.apache.tomcat.Jar;
+import org.apache.tomcat.util.scan.MultiReleaseUtil.MultiVersionInfo;
/**
* Base implementation of Jar for implementations that use a JarInputStream to
access the JAR file.
@@ -71,12 +71,7 @@ public abstract class AbstractInputStreamJar implements Jar {
// Skip base entries where there is a multi-release entry
// Skip multi-release entries that are not being used
while (entry != null && (mrMap.containsKey(entry.getName()) ||
- entry.getName().startsWith("META-INF/versions/") &&
!mrMap.containsValue(entry.getName()))) {
- entry = jarInputStream.getNextJarEntry();
- }
- } else {
- // Skip multi-release entries
- while (entry != null &&
entry.getName().startsWith("META-INF/versions/")) {
+ isVersionedEntry(entry.getName()) &&
!mrMap.containsValue(entry.getName()))) {
entry = jarInputStream.getNextJarEntry();
}
}
@@ -201,6 +196,16 @@ public abstract class AbstractInputStreamJar implements
Jar {
protected abstract NonClosingJarInputStream createJarInputStream() throws
IOException;
+ /*
+ * Entries below META-INF/versions/ that do not have a valid version
directory are not versioned entries. They are
+ * ordinary entries.
+ */
+ private static boolean isVersionedEntry(String name) {
+ MultiVersionInfo multiVersionInfo =
MultiReleaseUtil.parseVersion(name);
+ return multiVersionInfo != null && multiVersionInfo.isValid();
+ }
+
+
private void gotoEntry(String name) throws IOException {
boolean needsReset = true;
if (multiRelease == null) {
@@ -214,9 +219,6 @@ public abstract class AbstractInputStreamJar implements Jar
{
if (mrName != null) {
name = mrName;
}
- } else if (name.startsWith("META-INF/versions/")) {
- entry = null;
- return;
}
if (entry != null && name.equals(entry.getName())) {
@@ -241,6 +243,7 @@ public abstract class AbstractInputStreamJar implements Jar
{
int targetVersion = Runtime.version().feature();
Map<String,Integer> mrVersions = new HashMap<>();
+ Map<String,String> newMrMap = new HashMap<>();
JarEntry jarEntry = jarInputStream.getNextJarEntry();
@@ -248,34 +251,24 @@ public abstract class AbstractInputStreamJar implements
Jar {
// sufficient to be able to create the renaming map required
while (jarEntry != null) {
String name = jarEntry.getName();
- if (name.startsWith("META-INF/versions/") &&
name.endsWith(".class")) {
-
- // Get the base name and version for this versioned entry
- int i = name.indexOf('/', 18);
- if (i > 0) {
- String baseName = name.substring(i + 1);
- int version = 0;
- try {
- version = Integer.parseInt(name.substring(18, i));
- } catch (NumberFormatException e) {
- jarEntry = jarInputStream.getNextJarEntry();
- continue;
- }
-
- // Ignore any entries targeting for a later version than
- // the target for this runtime
- if (version <= targetVersion) {
- Integer mappedVersion = mrVersions.get(baseName);
- if (mappedVersion == null) {
- // No version found for this name. Create one.
- mrVersions.put(baseName, Integer.valueOf(version));
- } else {
- // Ignore any entry for which we have already found
- // a later version
- if (version > mappedVersion.intValue()) {
- // Replace the earlier version
- mrVersions.put(baseName,
Integer.valueOf(version));
- }
+ MultiVersionInfo multiVersionInfo =
MultiReleaseUtil.parseVersion(name);
+ if (multiVersionInfo != null && multiVersionInfo.isValid()) {
+ // Ignore any entries targeting for a later version than
+ // the target for this runtime
+ if (multiVersionInfo.version() <= targetVersion) {
+ String baseName =
name.substring(multiVersionInfo.namePos());
+ Integer mappedVersion = mrVersions.get(baseName);
+ if (mappedVersion == null) {
+ // No version found for this name. Create one.
+ mrVersions.put(baseName,
Integer.valueOf(multiVersionInfo.version()));
+ newMrMap.put(baseName, name);
+ } else {
+ // Ignore any entry for which we have already found
+ // a later version
+ if (multiVersionInfo.version() >
mappedVersion.intValue()) {
+ // Replace the earlier version
+ mrVersions.put(baseName,
Integer.valueOf(multiVersionInfo.version()));
+ newMrMap.put(baseName, name);
}
}
}
@@ -283,15 +276,9 @@ public abstract class AbstractInputStreamJar implements
Jar {
jarEntry = jarInputStream.getNextJarEntry();
}
- mrMap = new HashMap<>();
-
- for (Entry<String,Integer> mrVersion : mrVersions.entrySet()) {
- mrMap.put(mrVersion.getKey(),
- "META-INF/versions/" + mrVersion.getValue().toString() +
"/" + mrVersion.getKey());
- }
-
// Reset stream back to the beginning of the JAR
closeStream();
jarInputStream = createJarInputStream();
+ mrMap = newMrMap;
}
}
diff --git a/java/org/apache/tomcat/util/scan/JarFileUrlJar.java
b/java/org/apache/tomcat/util/scan/JarFileUrlJar.java
index b5a89464ae..2929a7e59a 100644
--- a/java/org/apache/tomcat/util/scan/JarFileUrlJar.java
+++ b/java/org/apache/tomcat/util/scan/JarFileUrlJar.java
@@ -34,6 +34,7 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.tomcat.Jar;
+import org.apache.tomcat.util.scan.MultiReleaseUtil.MultiVersionInfo;
/**
* Implementation of {@link Jar} that is optimised for file based JAR URLs
that refer directly to a JAR file (e.g. URLs
@@ -98,8 +99,7 @@ public class JarFileUrlJar implements Jar {
@Override
public InputStream getInputStream(String name) throws IOException {
- // JarFile#getEntry() is multi-release aware
- ZipEntry entry = jarFile.getEntry(name);
+ ZipEntry entry = getEntry(name);
if (entry == null) {
return null;
} else {
@@ -109,8 +109,7 @@ public class JarFileUrlJar implements Jar {
@Override
public long getLastModified(String name) throws IOException {
- // JarFile#getEntry() is multi-release aware
- ZipEntry entry = jarFile.getEntry(name);
+ ZipEntry entry = getEntry(name);
if (entry == null) {
return -1;
} else {
@@ -120,9 +119,25 @@ public class JarFileUrlJar implements Jar {
@Override
public boolean exists(String name) throws IOException {
- // JarFile#getEntry() is multi-release aware
- ZipEntry entry = jarFile.getEntry(name);
- return entry != null;
+ return getEntry(name) != null;
+ }
+
+ /*
+ * JarFile#getEntry() is only multi-release aware if the JarFile was
opened for the current runtime version. That is
+ * not the case when the JarFile is obtained from a jar: URL so, for
multi-release JARs, use the same look-up of the
+ * highest applicable version that is used when enumerating the entries.
+ */
+ private ZipEntry getEntry(String name) {
+ if (multiRelease) {
+ if (multiReleaseMap == null) {
+ multiReleaseMap = populateMultiReleaseMap();
+ }
+ JarEntry versionedEntry = multiReleaseMap.get(name);
+ if (versionedEntry != null) {
+ return versionedEntry;
+ }
+ }
+ return jarFile.getEntry(name);
}
@Override
@@ -175,13 +190,10 @@ public class JarFileUrlJar implements Jar {
if (entries.hasMoreElements()) {
entry = entries.nextElement();
name = entry.getName();
+ MultiVersionInfo multiVersionInfo =
MultiReleaseUtil.parseVersion(name);
// Get 'base' name
- if (name.startsWith("META-INF/versions/")) {
- int i = name.indexOf('/', 18);
- if (i == -1) {
- continue;
- }
- name = name.substring(i + 1);
+ if (multiVersionInfo != null &&
multiVersionInfo.isValid()) {
+ name = name.substring(multiVersionInfo.namePos());
}
if (name.isEmpty() || entryNamesSeen.contains(name)) {
continue;
@@ -271,27 +283,19 @@ public class JarFileUrlJar implements Jar {
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
String name = jarEntry.getName();
- if (name.startsWith("META-INF/versions/") &&
name.endsWith(".class")) {
- int i = name.indexOf('/', 18);
- if (i > 0) {
- String baseName = name.substring(i + 1);
- int version;
- try {
- version = Integer.parseInt(name, 18, i, 10);
- } catch (NumberFormatException e) {
- continue;
- }
+ MultiVersionInfo multiVersionInfo =
MultiReleaseUtil.parseVersion(name);
+ if (multiVersionInfo != null && multiVersionInfo.isValid()) {
+ String baseName = name.substring(multiVersionInfo.namePos());
- // Ignore any entries targeting for a later version than
- // the target for this runtime
- if (version <= targetVersion) {
- Integer mappedVersion = mrVersions.get(baseName);
- // Ignore any entry for which a later version has
- // already been found
- if (mappedVersion == null || version >
mappedVersion.intValue()) {
- mrVersions.put(baseName, Integer.valueOf(version));
- result.put(baseName, jarEntry);
- }
+ // Ignore any entries targeting for a later version than
+ // the target for this runtime
+ if (multiVersionInfo.version() <= targetVersion) {
+ Integer mappedVersion = mrVersions.get(baseName);
+ // Ignore any entry for which a later version has
+ // already been found
+ if (mappedVersion == null || multiVersionInfo.version() >
mappedVersion.intValue()) {
+ mrVersions.put(baseName,
Integer.valueOf(multiVersionInfo.version()));
+ result.put(baseName, jarEntry);
}
}
}
diff --git a/java/org/apache/tomcat/util/scan/MultiReleaseUtil.java
b/java/org/apache/tomcat/util/scan/MultiReleaseUtil.java
new file mode 100644
index 0000000000..09fd494292
--- /dev/null
+++ b/java/org/apache/tomcat/util/scan/MultiReleaseUtil.java
@@ -0,0 +1,97 @@
+/*
+ * 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;
+
+/**
+ * Utility class for working with multi-release JAR files.
+ */
+public class MultiReleaseUtil {
+
+ private static final String VERSION_PATH = "META-INF/versions/";
+ private static final int VERSION_PATH_LENGTH = VERSION_PATH.length();
+ private static final int MINIMUM_VERSIONED_JAVA = 8;
+
+
+ private MultiReleaseUtil() {
+ // Hide default constructor
+ }
+
+
+ /**
+ * Parses a JAR entry name and, if it represents a multi-release JAR
entry, returns the target Java version and the
+ * start point of the resource name in the JAR entry name. Version numbers
with leading zeros, signs or values less
+ * than 8 are ignored to match the JRE 21 onwards behaviour.
+ *
+ * @param name Name of the JAR entry
+ *
+ * @return The {@code MultiVersionInfo} for the entry if the entry
represents a versioned entry, otherwise
+ * {@code null}
+ */
+ public static MultiVersionInfo parseVersion(String name) {
+ if (name == null || name.isEmpty()) {
+ return null;
+ }
+ int start = 0;
+ if (name.charAt(0) == '/') {
+ start = 1;
+ }
+ if (name.startsWith(VERSION_PATH, start)) {
+ int end = name.indexOf('/', VERSION_PATH_LENGTH + start);
+ if (end > 0) {
+ for (int i = VERSION_PATH_LENGTH + start; i < end; i++) {
+ char c = name.charAt(i);
+ if (c < '0' || c > '9' || ((i == VERSION_PATH_LENGTH +
start) && c == '0')) {
+ return new MultiVersionInfo(-1, end + 1);
+ }
+ }
+ try {
+ int version = Integer.parseInt(name, VERSION_PATH_LENGTH +
start, end, 10);
+ if (version < MINIMUM_VERSIONED_JAVA) {
+ version = -1;
+ }
+ return new MultiVersionInfo(version, end + 1);
+ } catch (NumberFormatException e) {
+ // Must be overflow
+ return new MultiVersionInfo(-1, end + 1);
+ }
+ }
+ return new MultiVersionInfo(-1, -1);
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Provides the return value for calls to {@code
MultiVersionUtil#parseVersion(String)}.
+ *
+ * @param version The version or {@code -1} if the version was invalid
+ * @param namePos The start position of the name. The first character
after the '/' after the version. {@code -1} if
+ * there was no '/' after the version or the version
was not present
+ */
+ public record MultiVersionInfo(int version, int namePos) {
+
+ /**
+ * Has a valid multi-release entry been found?
+ *
+ * @return {@code true} if a valid multi-release entry was found,
otherwise {@code false}
+ */
+ public boolean isValid() {
+ return version >= 0;
+ }
+ }
+}
diff --git a/test/org/apache/catalina/webresources/TestJarContents.java
b/test/org/apache/catalina/webresources/TestJarContents.java
index edf1c1f756..3ed469a58d 100644
--- a/test/org/apache/catalina/webresources/TestJarContents.java
+++ b/test/org/apache/catalina/webresources/TestJarContents.java
@@ -151,10 +151,69 @@ public class TestJarContents {
// A leading slash is not significant
Assert.assertTrue(
jarContents.mightContainResource("/META-INF/versions/9x/lead.txt"));
- // Signed and valid version segments record the base name
- Assert.assertTrue(jarContents.mightContainResource("plus.txt"));
- Assert.assertTrue(jarContents.mightContainResource("minus.txt"));
+ // Signed version segments are invalid
+
Assert.assertTrue(jarContents.mightContainResource("META-INF/versions/+9/plus.txt"));
+
Assert.assertTrue(jarContents.mightContainResource("META-INF/versions/-9/minus.txt"));
+ // Valid version segments record the base name
Assert.assertTrue(jarContents.mightContainResource("ok.txt"));
}
}
+
+ @Test
+ public void testVersionedEntryForCurrentRuntime() throws Exception {
+ // An entry below the version directory for the running Java version
+ // is resolved under its base name. An entry targeting a later
+ // version is not applicable and remains under its full name.
+ int feature = Runtime.version().feature();
+ String current = "META-INF/versions/" + feature + "/current.txt";
+ String later = "META-INF/versions/" + (feature + 1) + "/later.txt";
+ File f = createJar(current, later);
+
+ try (JarFile jarFile = new JarFile(f)) {
+ JarContents jarContents = new JarContents(jarFile);
+
+ Assert.assertTrue(jarContents.mightContainResource("current.txt"));
+ Assert.assertTrue(jarContents.mightContainResource(later));
+ }
+ }
+
+ @Test
+ public void testLeadingZeroVersion() throws Exception {
+ // The JDK does not treat a version directory with a leading zero as a
+ // versioned entry so the entry stays under its full name.
+ File f = createJar("META-INF/versions/09/zero.txt");
+
+ try (JarFile jarFile = new JarFile(f)) {
+ JarContents jarContents = new JarContents(jarFile);
+
+
Assert.assertTrue(jarContents.mightContainResource("META-INF/versions/09/zero.txt"));
+ }
+ }
+
+ @Test
+ public void testEntryDirectlyBelowVersionsDirectory() throws Exception {
+ // There is no version directory so the entry stays under its full
+ // name and building the filter must not fail.
+ File f = createJar("META-INF/versions/Foo.class", "real.txt");
+
+ try (JarFile jarFile = new JarFile(f)) {
+ JarContents jarContents = new JarContents(jarFile);
+
+
Assert.assertTrue(jarContents.mightContainResource("META-INF/versions/Foo.class"));
+ Assert.assertTrue(jarContents.mightContainResource("real.txt"));
+ }
+ }
+
+ private static File createJar(String... names) throws Exception {
+ File f = File.createTempFile("test-jar-contents", ".jar");
+ f.deleteOnExit();
+ try (ZipOutputStream out = new ZipOutputStream(new
FileOutputStream(f))) {
+ for (String name : names) {
+ out.putNextEntry(new ZipEntry(name));
+ out.write("x".getBytes(StandardCharsets.UTF_8));
+ out.closeEntry();
+ }
+ }
+ return f;
+ }
}
diff --git a/test/org/apache/catalina/webresources/TestJarWarResourceSet.java
b/test/org/apache/catalina/webresources/TestJarWarResourceSet.java
index a93ad83fa0..b3a8631d3c 100644
--- a/test/org/apache/catalina/webresources/TestJarWarResourceSet.java
+++ b/test/org/apache/catalina/webresources/TestJarWarResourceSet.java
@@ -140,14 +140,96 @@ public class TestJarWarResourceSet extends TomcatBaseTest
{
resourceSet.gc();
}
+ @Test
+ public void testMultiReleaseVersionDirectoryRules() throws Exception {
+ // Version directories with a sign or a leading zero are not version
+ // directories as far as the JDK is concerned. An entry directly below
+ // META-INF/versions/ has no version directory at all. All of these
+ // stay in the index under their full name. A leading slash is not
+ // significant when identifying a versioned entry.
+ 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(createJar("META-INF/versions/+9/plus.txt",
"META-INF/versions/-9/minus.txt",
+ "META-INF/versions/09/zero.txt",
"META-INF/versions/Foo.class",
+ "/META-INF/versions/9/lead.txt",
"META-INF/versions/9/ok.txt", "real.txt"));
+ 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/+9/plus.txt"));
+ Assert.assertFalse(entries.containsKey("plus.txt"));
+
Assert.assertTrue(entries.containsKey("META-INF/versions/-9/minus.txt"));
+ Assert.assertFalse(entries.containsKey("minus.txt"));
+
Assert.assertTrue(entries.containsKey("META-INF/versions/09/zero.txt"));
+ Assert.assertFalse(entries.containsKey("zero.txt"));
+ Assert.assertTrue(entries.containsKey("META-INF/versions/Foo.class"));
+ Assert.assertTrue(entries.containsKey("lead.txt"));
+ Assert.assertTrue(entries.containsKey("ok.txt"));
+ Assert.assertTrue(entries.containsKey("real.txt"));
+
+ // The bloom filter has to agree with the index
+ JarContents jarContents = resourceSet.jarContents;
+ Assert.assertNotNull(jarContents);
+
Assert.assertTrue(jarContents.mightContainResource("META-INF/versions/+9/plus.txt"));
+
Assert.assertTrue(jarContents.mightContainResource("META-INF/versions/-9/minus.txt"));
+
Assert.assertTrue(jarContents.mightContainResource("META-INF/versions/09/zero.txt"));
+
Assert.assertTrue(jarContents.mightContainResource("META-INF/versions/Foo.class"));
+ Assert.assertTrue(jarContents.mightContainResource("lead.txt"));
+ Assert.assertTrue(jarContents.mightContainResource("ok.txt"));
+
+
Assert.assertTrue(resourceSet.getResource("/WEB-INF/classes/META-INF/versions/+9/plus.txt").exists());
+
Assert.assertTrue(resourceSet.getResource("/WEB-INF/classes/META-INF/versions/09/zero.txt").exists());
+
Assert.assertTrue(resourceSet.getResource("/WEB-INF/classes/META-INF/versions/Foo.class").exists());
+
Assert.assertTrue(resourceSet.getResource("/WEB-INF/classes/lead.txt").exists());
+
+ resourceSet.gc();
+ }
+
+ @Test
+ public void testMultiReleaseVersionDirectoryEntry() throws Exception {
+ // The entry for the version directory itself has no resource name so
+ // it must not be added to the index as an entry with an empty 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(createJar("META-INF/versions/9/",
"META-INF/versions/9/ok.txt", "real.txt"));
+ 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.assertFalse(entries.containsKey(""));
+ Assert.assertTrue(entries.containsKey("ok.txt"));
+ Assert.assertTrue(entries.containsKey("real.txt"));
+
Assert.assertTrue(resourceSet.getResource("/WEB-INF/classes/ok.txt").exists());
+
+ resourceSet.gc();
+ }
+
private static byte[] createMultiReleaseJar() throws IOException {
+ return createJar("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");
+ }
+
+ private static byte[] createJar(String... names) 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) {
diff --git a/test/org/apache/tomcat/util/scan/TestAbstractInputStreamJar.java
b/test/org/apache/tomcat/util/scan/TestAbstractInputStreamJar.java
index 3ffe7eebd9..5db1bf3740 100644
--- a/test/org/apache/tomcat/util/scan/TestAbstractInputStreamJar.java
+++ b/test/org/apache/tomcat/util/scan/TestAbstractInputStreamJar.java
@@ -18,12 +18,22 @@ package org.apache.tomcat.util.scan;
import java.io.ByteArrayOutputStream;
import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+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.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.apache.catalina.util.IOTools;
import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory;
@@ -31,6 +41,12 @@ import org.apache.tomcat.Jar;
public class TestAbstractInputStreamJar {
+ private static final String VERSIONS = "META-INF/versions/";
+ private static final String BASE_CLASS = "test/Foo.class";
+
+ @Rule
+ public TemporaryFolder testDir = new TemporaryFolder();
+
@Before
public void register() {
TomcatURLStreamHandlerFactory.register();
@@ -56,4 +72,210 @@ public class TestAbstractInputStreamJar {
Assert.assertArrayEquals(baos1.toByteArray(), baos2.toByteArray());
}
+
+
+ /*
+ * Creates a multi-release JAR. The content of each entry is its own name
so the entry that was read can be
+ * identified.
+ */
+ private Jar createJar(String... entryNames) throws IOException {
+ return createJar(true, entryNames);
+ }
+
+ private Jar createJar(boolean multiRelease, String... entryNames) throws
IOException {
+ Manifest manifest = new Manifest();
+ manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION,
"1.0");
+ if (multiRelease) {
+ manifest.getMainAttributes().putValue("Multi-Release", "true");
+ }
+
+ File file = new File(testDir.getRoot(), "mr.jar");
+ try (JarOutputStream jos = new JarOutputStream(new
FileOutputStream(file), manifest)) {
+ for (String entryName : entryNames) {
+ jos.putNextEntry(new JarEntry(entryName));
+ jos.write(entryName.getBytes("ISO-8859-1"));
+ jos.closeEntry();
+ }
+ }
+ return new UrlJar(URI.create("jar:" +
file.toURI().toURL().toExternalForm() + "!/").toURL());
+ }
+
+ private static String read(Jar jar, String name) throws IOException {
+ try (InputStream is = jar.getInputStream(name)) {
+ if (is == null) {
+ return null;
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ IOTools.flow(is, baos);
+ return baos.toString("ISO-8859-1");
+ }
+ }
+
+ private static List<String> entryNames(Jar jar) {
+ List<String> names = new ArrayList<>();
+ jar.nextEntry();
+ while (jar.getEntryName() != null) {
+ names.add(jar.getEntryName());
+ jar.nextEntry();
+ }
+ return names;
+ }
+
+
+ @Test
+ public void testMultiReleaseVersionSelected() throws Exception {
+ String v9 = VERSIONS + "9/" + BASE_CLASS;
+ String v11 = VERSIONS + "11/" + BASE_CLASS;
+ try (Jar jar = createJar(v9, v11, BASE_CLASS)) {
+ // Requests for the base name are served from the highest
applicable version
+ Assert.assertEquals(v11, read(jar, BASE_CLASS));
+
+ jar.reset();
+ List<String> names = entryNames(jar);
+ Assert.assertTrue(names.contains(v11));
+ Assert.assertFalse(names.contains(v9));
+ Assert.assertFalse(names.contains(BASE_CLASS));
+ }
+ }
+
+
+ /*
+ * A leading '/' is not significant when identifying a versioned entry.
The base entry must remain reachable and
+ * must be served from the versioned entry, using the entry's real name.
+ */
+ @Test
+ public void testMultiReleaseLeadingSlash() throws Exception {
+ String versioned = "/" + VERSIONS + "9/" + BASE_CLASS;
+ try (Jar jar = createJar(versioned, BASE_CLASS)) {
+ Assert.assertEquals(versioned, read(jar, BASE_CLASS));
+ }
+ }
+
+
+ /*
+ * Version directories that are not used by the JDK (sign, leading zero,
below the minimum, not a number) are not
+ * versioned entries. The base entry must be served from the base entry.
+ */
+ @Test
+ public void testMultiReleaseIgnoredVersionDirectories() throws Exception {
+ String[] versions = { "+9", "-9", "09", "7", "1", "abc",
"99999999999999" };
+ for (String version : versions) {
+ try (Jar jar = createJar(VERSIONS + version + "/" + BASE_CLASS,
BASE_CLASS)) {
+ Assert.assertEquals(version, BASE_CLASS, read(jar,
BASE_CLASS));
+
+ jar.reset();
+ Assert.assertTrue(version,
entryNames(jar).contains(BASE_CLASS));
+ }
+ }
+ }
+
+
+ @Test
+ public void testMultiReleaseEntryDirectlyBelowVersionsDirectory() throws
Exception {
+ try (Jar jar = createJar(VERSIONS + "Foo.class", VERSIONS + "9/" +
BASE_CLASS, BASE_CLASS)) {
+ Assert.assertEquals(VERSIONS + "9/" + BASE_CLASS, read(jar,
BASE_CLASS));
+
+ jar.reset();
+ List<String> names = entryNames(jar);
+ Assert.assertTrue(names.contains(VERSIONS + "9/" + BASE_CLASS));
+ Assert.assertFalse(names.contains(BASE_CLASS));
+ }
+ }
+
+
+ /*
+ * Entries below META-INF/versions/ that are not versioned entries (the
version directory is a sign, has a leading
+ * zero, is below the minimum, is not a number, or is missing) are
ordinary entries. They must be returned under
+ * their full name, as JarFileUrlJar does, and must be readable by that
name.
+ */
+ @Test
+ public void testMultiReleaseUnversionedEntriesAreReturned() throws
Exception {
+ String[] unversioned = { VERSIONS + "+9/" + BASE_CLASS, VERSIONS +
"-9/" + BASE_CLASS,
+ VERSIONS + "09/" + BASE_CLASS, VERSIONS + "7/" + BASE_CLASS,
VERSIONS + "abc/" + BASE_CLASS,
+ VERSIONS + "99999999999999/" + BASE_CLASS, VERSIONS +
"Foo.class" };
+
+ for (String entryName : unversioned) {
+ try (Jar jar = createJar(entryName, BASE_CLASS)) {
+ List<String> names = entryNames(jar);
+ Assert.assertTrue(entryName, names.contains(entryName));
+ Assert.assertTrue(entryName, names.contains(BASE_CLASS));
+
+ Assert.assertEquals(entryName, entryName, read(jar,
entryName));
+ Assert.assertEquals(entryName, BASE_CLASS, read(jar,
BASE_CLASS));
+ }
+ }
+ }
+
+
+ /*
+ * Nothing is versioned in a JAR that is not a multi-release JAR. Every
entry must be returned, in order, and every
+ * entry must be readable by name, including those below
META-INF/versions/.
+ */
+ @Test
+ public void testNotMultiRelease() throws Exception {
+ String versioned = VERSIONS + "9/" + BASE_CLASS;
+ String[] entryNames = { "a.txt", "b.txt", versioned, BASE_CLASS,
"c.txt", "d.txt", "e.txt" };
+
+ try (Jar jar = createJar(false, entryNames)) {
+ Assert.assertEquals(List.of(entryNames), entryNames(jar));
+
+ for (String entryName : entryNames) {
+ Assert.assertEquals(entryName, entryName, read(jar,
entryName));
+ }
+
+ jar.reset();
+ Assert.assertEquals(List.of(entryNames), entryNames(jar));
+ }
+ }
+
+
+ /*
+ * An entry for a lower version is not used when a higher version is
available. That must also be the case when the
+ * entry name has a leading '/'.
+ */
+ @Test
+ public void testMultiReleaseUnselectedLeadingSlashEntryIsHidden() throws
Exception {
+ String lower = "/" + VERSIONS + "9/" + BASE_CLASS;
+ String higher = VERSIONS + "11/" + BASE_CLASS;
+ try (Jar jar = createJar(lower, higher, BASE_CLASS)) {
+ Assert.assertEquals(higher, read(jar, BASE_CLASS));
+
+ jar.reset();
+ Assert.assertEquals(List.of(higher), entryNames(jar));
+ }
+ }
+
+
+ /*
+ * Multi-release applies to all types of resource, not just classes. The
base name must be served from the highest
+ * version that applies to the current runtime.
+ */
+ @Test
+ public void testMultiReleaseVersionedResourceIsUsed() throws Exception {
+ String base = "test/resource.txt";
+ String v9 = VERSIONS + "9/" + base;
+ String v11 = VERSIONS + "11/" + base;
+ try (Jar jar = createJar(v9, v11, base)) {
+ Assert.assertEquals(v11, read(jar, base));
+
+ jar.reset();
+ Assert.assertEquals(List.of(v11), entryNames(jar));
+ }
+ }
+
+
+ /*
+ * A resource that only exists in the versions location is available under
its base name.
+ */
+ @Test
+ public void testMultiReleaseVersionedResourceWithoutBaseEntry() throws
Exception {
+ String base = "test/resource.txt";
+ String v9 = VERSIONS + "9/" + base;
+ try (Jar jar = createJar(v9)) {
+ Assert.assertEquals(v9, read(jar, base));
+
+ jar.reset();
+ Assert.assertEquals(List.of(v9), entryNames(jar));
+ }
+ }
}
diff --git a/test/org/apache/tomcat/util/scan/TestJarFileUrlJar.java
b/test/org/apache/tomcat/util/scan/TestJarFileUrlJar.java
index 9165d5e70c..6b69698491 100644
--- a/test/org/apache/tomcat/util/scan/TestJarFileUrlJar.java
+++ b/test/org/apache/tomcat/util/scan/TestJarFileUrlJar.java
@@ -182,9 +182,8 @@ public class TestJarFileUrlJar {
/*
* Re-enumeration after reset() must produce the same selection. Entries
under a version directory that cannot be
- * parsed must not be returned. Versioned entries for resources (non-class
entries) are handled the same way as by
- * the stream based implementations: the base entry is used where one
exists and the versioned entries are
- * ignored.
+ * parsed must be treated as normal entries. Versioned entries for
resources (non-class entries) are handled the
+ * same way as versioned classes: the highest applicable version is used
in place of the base entry.
*/
@Test
public void testMultiReleaseResetAndUnusableEntries() throws Exception {
@@ -196,12 +195,12 @@ public class TestJarFileUrlJar {
VERSIONS + "11/" + BASE_CLASS, BASE_CLASS, resource);
try (Jar jar = new JarFileUrlJar(file.toURI().toURL(), false)) {
List<String> names = entryNames(jar);
- Assert.assertFalse(names.contains(unparseable));
+ Assert.assertTrue(names.contains(unparseable));
Assert.assertFalse(names.contains(VERSIONS + "9/" + BASE_CLASS));
Assert.assertTrue(names.contains(VERSIONS + "11/" + BASE_CLASS));
Assert.assertFalse(names.contains(BASE_CLASS));
- Assert.assertFalse(names.contains(versionedResource));
- Assert.assertTrue(names.contains(resource));
+ Assert.assertTrue(names.contains(versionedResource));
+ Assert.assertFalse(names.contains(resource));
jar.reset();
Assert.assertEquals(names, entryNames(jar));
@@ -223,4 +222,191 @@ public class TestJarFileUrlJar {
Assert.assertFalse(names.contains(BASE_CLASS));
}
}
+
+
+ /*
+ * An entry directly below META-INF/versions/ has no version directory. It
must not stop the JAR being scanned and
+ * is returned under its full name like any other entry that is not a
versioned entry.
+ */
+ @Test
+ public void testMultiReleaseEntryDirectlyBelowVersionsDirectory() throws
Exception {
+ File file = createMultiReleaseJar(VERSIONS + "Foo.class", VERSIONS +
"9/" + BASE_CLASS, BASE_CLASS);
+ try (Jar jar = new JarFileUrlJar(file.toURI().toURL(), false)) {
+ List<String> names = entryNames(jar);
+ Assert.assertTrue(names.contains(VERSIONS + "Foo.class"));
+ Assert.assertTrue(names.contains(VERSIONS + "9/" + BASE_CLASS));
+ Assert.assertFalse(names.contains(BASE_CLASS));
+ }
+ }
+
+
+ /*
+ * Version directories with a sign or a leading zero are not version
directories as far as the JDK is concerned.
+ * The entries below them are ordinary entries so they are returned under
their full name and must not be selected
+ * in place of the base entry.
+ */
+ @Test
+ public void testMultiReleaseSignedAndLeadingZeroVersionsAreNotVersioned()
throws Exception {
+ String[] versions = { "+9", "-9", "09" };
+ for (String version : versions) {
+ File file = createMultiReleaseJar(VERSIONS + version + "/" +
BASE_CLASS, BASE_CLASS);
+ try (Jar jar = new JarFileUrlJar(file.toURI().toURL(), false)) {
+ List<String> names = entryNames(jar);
+ Assert.assertTrue(version, names.contains(VERSIONS + version +
"/" + BASE_CLASS));
+ Assert.assertTrue(version, names.contains(BASE_CLASS));
+ }
+ }
+ }
+
+
+ /*
+ * A leading '/' is not significant when identifying a versioned entry.
+ */
+ @Test
+ public void testMultiReleaseLeadingSlash() throws Exception {
+ String versioned = "/" + VERSIONS + "9/" + BASE_CLASS;
+ File file = createMultiReleaseJar(versioned, BASE_CLASS);
+ try (Jar jar = new JarFileUrlJar(file.toURI().toURL(), false)) {
+ List<String> names = entryNames(jar);
+ Assert.assertTrue(names.contains(versioned));
+ Assert.assertFalse(names.contains(BASE_CLASS));
+ }
+ }
+
+
+ /*
+ * Multi-release applies to all types of resource, not just classes. The
entry selected must be the highest version
+ * that applies to the current runtime, regardless of the order in which
the entries appear in the JAR.
+ */
+ @Test
+ public void testMultiReleaseVersionSelectionForResources() throws
Exception {
+ String base = "test/resource.txt";
+ String v9 = VERSIONS + "9/" + base;
+ String v11 = VERSIONS + "11/" + base;
+
+ File file = createMultiReleaseJar(v9, v11, base);
+ try (Jar jar = new JarFileUrlJar(file.toURI().toURL(), false)) {
+ List<String> names = entryNames(jar);
+ Assert.assertTrue(names.contains(v11));
+ Assert.assertFalse(names.contains(v9));
+ Assert.assertFalse(names.contains(base));
+ }
+
+ file = createMultiReleaseJar(base, v11, v9);
+ try (Jar jar = new JarFileUrlJar(file.toURI().toURL(), false)) {
+ List<String> names = entryNames(jar);
+ Assert.assertTrue(names.contains(v11));
+ Assert.assertFalse(names.contains(v9));
+ Assert.assertFalse(names.contains(base));
+ }
+ }
+
+
+ /*
+ * A resource that only exists in the versions location is available.
+ */
+ @Test
+ public void testMultiReleaseVersionedResourceWithoutBaseEntry() throws
Exception {
+ String v9 = VERSIONS + "9/test/resource.txt";
+
+ File file = createMultiReleaseJar(v9);
+ try (Jar jar = new JarFileUrlJar(file.toURI().toURL(), false)) {
+ Assert.assertTrue(entryNames(jar).contains(v9));
+ }
+ }
+
+
+ /*
+ * Creates a multi-release JAR where the content of each entry is its own
name so the entry that was selected can be
+ * identified from the content.
+ */
+ private File createMultiReleaseJarWithNameContent(String... entryNames)
throws IOException {
+ Manifest manifest = new Manifest();
+ manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION,
"1.0");
+ manifest.getMainAttributes().putValue("Multi-Release", "true");
+
+ File file = new File(testDir.getRoot(), "named.jar");
+ try (JarOutputStream jos = new JarOutputStream(new
FileOutputStream(file), manifest)) {
+ for (String entryName : entryNames) {
+ jos.putNextEntry(new JarEntry(entryName));
+ jos.write(entryName.getBytes("ISO-8859-1"));
+ jos.closeEntry();
+ }
+ }
+ return file;
+ }
+
+
+ private static String read(Jar jar, String name) throws IOException {
+ try (InputStream is = jar.getInputStream(name)) {
+ return is == null ? null : new String(is.readAllBytes(),
"ISO-8859-1");
+ }
+ }
+
+
+ /*
+ * Look-ups by name must be multi-release aware for every type of
resource: the highest version that applies to the
+ * current runtime is used, versions for later runtimes are ignored and a
resource that only exists in the versions
+ * location is available.
+ */
+ private void doTestMultiReleaseLookupByName(Jar jar) throws IOException {
+ String resource = "test/resource.txt";
+ String v11Resource = VERSIONS + "11/" + resource;
+ String only9 = "only9.txt";
+ String v9Only9 = VERSIONS + "9/" + only9;
+ String laterVersion = "later.txt";
+
+ // Highest applicable version used in place of the base entry or a
lower version
+ Assert.assertEquals(v11Resource, read(jar, resource));
+ Assert.assertTrue(jar.exists(resource));
+ Assert.assertTrue(jar.getLastModified(resource) != -1);
+
+ // Class
+ Assert.assertEquals(VERSIONS + "11/" + BASE_CLASS, read(jar,
BASE_CLASS));
+
+ // Only in the versions location
+ Assert.assertEquals(v9Only9, read(jar, only9));
+ Assert.assertTrue(jar.exists(only9));
+ Assert.assertTrue(jar.getLastModified(only9) != -1);
+
+ // Version for a later runtime ignored
+ Assert.assertEquals(laterVersion, read(jar, laterVersion));
+
+ // Not present at all
+ Assert.assertNull(read(jar, "missing.txt"));
+ Assert.assertFalse(jar.exists("missing.txt"));
+ Assert.assertEquals(-1, jar.getLastModified("missing.txt"));
+
+ // Entry that only exists for a later runtime is not available
+ Assert.assertNull(read(jar, "onlyLater.txt"));
+ Assert.assertFalse(jar.exists("onlyLater.txt"));
+ }
+
+
+ private File createLookupJar() throws IOException {
+ String resource = "test/resource.txt";
+ return createMultiReleaseJarWithNameContent(resource, VERSIONS + "9/"
+ resource, VERSIONS + "11/" + resource,
+ BASE_CLASS, VERSIONS + "9/" + BASE_CLASS, VERSIONS + "11/" +
BASE_CLASS,
+ VERSIONS + "9/only9.txt", "later.txt", VERSIONS +
"999/later.txt",
+ VERSIONS + "999/onlyLater.txt");
+ }
+
+
+ @Test
+ public void testMultiReleaseLookupByName() throws Exception {
+ File file = createLookupJar();
+ try (Jar jar = new JarFileUrlJar(file.toURI().toURL(), false)) {
+ doTestMultiReleaseLookupByName(jar);
+ }
+ }
+
+
+ @Test
+ public void testMultiReleaseLookupByNameViaJarUrl() throws Exception {
+ File file = createLookupJar();
+ URL url = URI.create("jar:" + file.toURI().toURL().toExternalForm() +
"!/").toURL();
+ try (Jar jar = new JarFileUrlJar(url, true)) {
+ doTestMultiReleaseLookupByName(jar);
+ }
+ }
}
diff --git a/test/org/apache/tomcat/util/scan/TestMultiVersionUtil.java
b/test/org/apache/tomcat/util/scan/TestMultiVersionUtil.java
new file mode 100644
index 0000000000..3156bdf39d
--- /dev/null
+++ b/test/org/apache/tomcat/util/scan/TestMultiVersionUtil.java
@@ -0,0 +1,134 @@
+/*
+ * 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 org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.tomcat.util.scan.MultiReleaseUtil.MultiVersionInfo;
+
+public class TestMultiVersionUtil {
+
+ @Test
+ public void testValidVersion() {
+ MultiVersionInfo info =
MultiReleaseUtil.parseVersion("META-INF/versions/9/test.txt");
+ Assert.assertNotNull(info);
+ Assert.assertTrue(info.isValid());
+ Assert.assertEquals(9, info.version());
+ Assert.assertEquals("test.txt",
"META-INF/versions/9/test.txt".substring(info.namePos()));
+ }
+
+
+ /*
+ * The JDK does not treat a version directory with a leading zero as a
versioned entry (it resolves the base name
+ * to the base entry, not to the entry below META-INF/versions/09/) so
neither should Tomcat.
+ */
+ @Test
+ public void testLeadingZeroVersionIsInvalid() {
+ MultiVersionInfo info =
MultiReleaseUtil.parseVersion("META-INF/versions/09/test.txt");
+ Assert.assertNotNull(info);
+ Assert.assertFalse(info.isValid());
+ }
+
+
+ @Test
+ public void testCurrentRuntimeVersion() {
+ int feature = Runtime.version().feature();
+ MultiVersionInfo info =
MultiReleaseUtil.parseVersion("META-INF/versions/" + feature + "/test.txt");
+ Assert.assertNotNull(info);
+ Assert.assertTrue(info.isValid());
+ Assert.assertEquals(feature, info.version());
+ }
+
+
+ @Test
+ public void testNotVersioned() {
+ Assert.assertNull(MultiReleaseUtil.parseVersion(null));
+ Assert.assertNull(MultiReleaseUtil.parseVersion(""));
+ Assert.assertNull(MultiReleaseUtil.parseVersion("test/Foo.class"));
+
Assert.assertNull(MultiReleaseUtil.parseVersion("META-INF/MANIFEST.MF"));
+ // No trailing '/' so not the versions directory
+ Assert.assertNull(MultiReleaseUtil.parseVersion("META-INF/versions"));
+
Assert.assertNull(MultiReleaseUtil.parseVersion("other/META-INF/versions/9/test.txt"));
+ }
+
+
+ @Test
+ public void testLeadingSlash() {
+ String name = "/META-INF/versions/9/test.txt";
+ MultiVersionInfo info = MultiReleaseUtil.parseVersion(name);
+ Assert.assertNotNull(info);
+ Assert.assertTrue(info.isValid());
+ Assert.assertEquals(9, info.version());
+ Assert.assertEquals("test.txt", name.substring(info.namePos()));
+ }
+
+
+ @Test
+ public void testDirectoryEntry() {
+ String name = "META-INF/versions/9/";
+ MultiVersionInfo info = MultiReleaseUtil.parseVersion(name);
+ Assert.assertNotNull(info);
+ Assert.assertTrue(info.isValid());
+ Assert.assertEquals(name.length(), info.namePos());
+ }
+
+
+ @Test
+ public void testInvalidVersions() {
+ String[] versions = { "+9", "-9", "9x", "x9", "9.0", "", "0", "09",
"00", "99999999999999", "2147483648" };
+ for (String version : versions) {
+ String name = "META-INF/versions/" + version + "/test.txt";
+ MultiVersionInfo info = MultiReleaseUtil.parseVersion(name);
+ Assert.assertNotNull(version, info);
+ Assert.assertFalse(version, info.isValid());
+ // The position of the resource name is still reported so callers
can keep the entry under its full name
+ Assert.assertEquals(version, "test.txt",
name.substring(info.namePos()));
+ }
+ }
+
+
+ @Test
+ public void testNoSeparatorAfterVersionsDirectory() {
+ MultiVersionInfo info =
MultiReleaseUtil.parseVersion("META-INF/versions/Foo.class");
+ Assert.assertNotNull(info);
+ Assert.assertFalse(info.isValid());
+ Assert.assertEquals(-1, info.namePos());
+ }
+
+
+ /*
+ * Java 21 onwards treats 8 as the lowest version directory that is used.
Lower versions are ignored.
+ */
+ @Test
+ public void testMinimumVersion() {
+ for (int version = 1; version < 8; version++) {
+ String name = "META-INF/versions/" + version + "/test.txt";
+ MultiVersionInfo info = MultiReleaseUtil.parseVersion(name);
+ Assert.assertNotNull(Integer.toString(version), info);
+ Assert.assertFalse(Integer.toString(version), info.isValid());
+ Assert.assertEquals(Integer.toString(version), "test.txt",
name.substring(info.namePos()));
+ }
+
+ for (int version = 8; version <= 10; version++) {
+ MultiVersionInfo info =
MultiReleaseUtil.parseVersion("META-INF/versions/" + version + "/test.txt");
+ Assert.assertNotNull(Integer.toString(version), info);
+ Assert.assertTrue(Integer.toString(version), info.isValid());
+ Assert.assertEquals(version, info.version());
+ }
+ }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index bf8ee6ea00..f63cd8e5c0 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -484,6 +484,13 @@
<code>TIME</code> is now of the form <code>yyyyMMddHHmmss</code>
rather than an HTTP date.
</fix>
+ <fix>
+ Refactor to reduce duplication and improve consistency when working
with
+ resources in multi-release JAR files. Versions that include signs,
+ leading zeros or values less than 8 are no longer considered valid. All
+ types of resources, not just class files, are now handled in
+ multi-release JAR files. (markt)
+ </fix>
</changelog>
</subsection>
<subsection name="Coyote">
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]