You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by re...@apache.org on 2026/10/08 15:48:09 UTC
(tomcat) branch 11.0.x updated (5f7b6efaef -> 27c32481e2)
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a change to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
from 5f7b6efaef Follow up to "Keep naming resource entries consistent when updated..."
new ea6775810b Fix StringIndexOutOfBoundsException for unterminated regular expression in SSI expressions
new 1190183f58 Report a parse error for SSI expressions with missing operands
new 0ff7f766ae Give && higher precedence than || in SSI conditional expressions
new c8664c6796 Restore the interrupt status when SSI #exec cmd is interrupted
new 214ee2d345 Report a parse error for unterminated literals in SSI expressions
new 3341e7f1b1 Fix SSI built-in date variables always resolving to null
new 32cf81e2f2 Make SSI #fsize output independent of the JVM default locale
new 925493100c Accept the var and value attributes of SSI #set in any order
new 57b551577f Keep the Last-Modified header for large unbuffered SSI documents
new 68788f48ff Require both var and value attributes for the SSI #set directive
new 0265a5d6e8 Report the configured error message when a conditional SSI directive fails
new bd06bc435c Fix data races in parallel annotation scanning
new ae2d22b8da Fall back to classpath defaults when catalina.properties is corrupt
new 4dfe4ca992 Send the server shutdown command as UTF-8 and time out the stop connect
new f2328d84e9 Skip parsing a context configuration file that cannot be opened
new 579a90cca3 Use a consistent key when adding and removing serviced entries during legacy application migration.
new 37ccf9feb6 Restore the previous web application when legacy application migration cannot place the migrated application.
new 186fca662f Return an absolute home path from HomesUserDatabase when homeBase is relative.
new fd3fbc7cbf Remove the incomplete WAR left behind when copying a WAR to the appBase fails.
new 09bcd34a7b Correct the Tomcat.init() Javadoc regarding the configuration source.
new 3092acd16c Normalise a context path missing its leading slash in ContextName.
new 172dc23438 Report service provider loading failures caused by a LinkageError via the documented IOException contract.
new 27c32481e2 Match Jakarta namespace JMS types when classifying @Resource annotations.
The 23 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
java/org/apache/catalina/core/StandardServer.java | 14 +-
.../apache/catalina/ssi/ExpressionParseTree.java | 91 +++++++++---
.../apache/catalina/ssi/ExpressionTokenizer.java | 13 +-
.../apache/catalina/ssi/LocalStrings.properties | 7 +
java/org/apache/catalina/ssi/SSIConditional.java | 16 ++-
java/org/apache/catalina/ssi/SSIExec.java | 3 +
java/org/apache/catalina/ssi/SSIFsize.java | 7 +-
java/org/apache/catalina/ssi/SSIMediator.java | 67 ++++-----
java/org/apache/catalina/ssi/SSIProcessor.java | 7 +-
java/org/apache/catalina/ssi/SSIServlet.java | 11 +-
java/org/apache/catalina/ssi/SSISet.java | 30 ++--
java/org/apache/catalina/startup/Catalina.java | 25 +++-
.../catalina/startup/CatalinaProperties.java | 57 +++++---
.../org/apache/catalina/startup/ContextConfig.java | 49 +++++--
.../apache/catalina/startup/HomesUserDatabase.java | 2 +-
java/org/apache/catalina/startup/HostConfig.java | 57 +++++++-
.../catalina/startup/LocalStrings.properties | 3 +
java/org/apache/catalina/startup/Tomcat.java | 24 +++-
.../apache/catalina/startup/WebAnnotationSet.java | 12 +-
.../catalina/startup/WebappServiceLoader.java | 5 +
java/org/apache/catalina/util/ContextName.java | 10 +-
.../catalina/ssi/TestExpressionParseTree.java | 70 +++++++++
.../catalina/ssi/TestExpressionTokenizer.java | 13 ++
test/org/apache/catalina/ssi/TestSSIFsize.java | 105 ++++++++++++++
test/org/apache/catalina/ssi/TestSSISet.java | 158 +++++++++++++++++++++
test/org/apache/catalina/ssi/TestSsiServlet.java | 85 +++++++++++
.../catalina/startup/TestWebappServiceLoader.java | 20 +++
...va => TesterServiceWithFailingInitializer.java} | 22 +--
test/org/apache/catalina/util/TestContextName.java | 13 ++
webapps/docs/changelog.xml | 49 +++++++
webapps/docs/ssi-howto.xml | 6 +
31 files changed, 905 insertions(+), 146 deletions(-)
create mode 100644 test/org/apache/catalina/ssi/TestSSIFsize.java
create mode 100644 test/org/apache/catalina/ssi/TestSSISet.java
copy test/org/apache/catalina/startup/{TesterServletContainerInitializer2.java => TesterServiceWithFailingInitializer.java} (67%)
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 15/23: Skip parsing a context configuration file that cannot be opened
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit f2328d84e9d50ae9414c12298f41f848d452ca24
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 15:48:47 2026 +0200
Skip parsing a context configuration file that cannot be opened
In processContextConfig() the InputSource was created before the
input stream for the configuration file was opened, so the null check
that guarded the parse was unreachable. When opening the file failed,
for example an external context configuration file that does not
exist, processing continued into digester.parse() with a byte-stream
less InputSource, the SAX parser re-resolved the same URL and failed
again, logging a second, duplicate error for the one problem.
The stream is now opened before the InputSource is created. If
opening fails, the single error is logged, the context is marked as
not configured and parsing is skipped. The visible outcome for the
deployment is unchanged; only the duplicate error output is removed.
---
java/org/apache/catalina/startup/ContextConfig.java | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/java/org/apache/catalina/startup/ContextConfig.java b/java/org/apache/catalina/startup/ContextConfig.java
index 1d18e72359..6c8326bf18 100644
--- a/java/org/apache/catalina/startup/ContextConfig.java
+++ b/java/org/apache/catalina/startup/ContextConfig.java
@@ -776,22 +776,21 @@ public class ContextConfig implements LifecycleListener {
log.debug(sm.getString("contextConfig.processContext", context.getName(), contextXml));
}
- InputSource source = null;
+ InputSource source;
- try {
- source = new InputSource(contextXml.toString());
- if (stream == null) {
+ if (stream == null) {
+ try {
URLConnection xmlConn = contextXml.openConnection();
xmlConn.setUseCaches(false);
stream = xmlConn.getInputStream();
+ } catch (Exception e) {
+ log.error(sm.getString("contextConfig.contextMissing", contextXml), e);
+ ok = false;
+ return;
}
- } catch (Exception e) {
- log.error(sm.getString("contextConfig.contextMissing", contextXml), e);
}
- if (source == null) {
- return;
- }
+ source = new InputSource(contextXml.toString());
try {
source.setByteStream(stream);
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 06/23: Fix SSI built-in date variables always resolving to null
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 3341e7f1b1942055d2bd7a7089090a989f604feb
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 14:19:01 2026 +0200
Fix SSI built-in date variables always resolving to null
The mediator stored the built-in variables DATE_GMT, DATE_LOCAL and
LAST_MODIFIED as external variables using names starting with its own
fully qualified class name, and looked them up the same way. The servlet
resolver rejects names under the reserved prefixes, and since
org.apache.catalina.ssi.SSIMediator falls under the
org.apache.catalina. prefix added when the reserved list was extended,
both the store and the lookup silently did nothing and the variables
always rendered as (none).
Hold the built-in variables in the mediator itself, as per-document
values that do not need to live in the external resolver, and resolve
them there after the external lookup, preserving the ability of the
document to override them via #set. The cross-mediator alreadyset guard
was dead (it used the same rejected names) and has no meaning with
per-mediator storage; with it gone, the fromConstructor parameter of
setConfigTimeFmt() and setDateVariables() is no longer needed and is
removed.
---
java/org/apache/catalina/ssi/SSIMediator.java | 67 ++++++++++--------------
test/org/apache/catalina/ssi/TestSsiServlet.java | 3 ++
webapps/docs/changelog.xml | 12 +++++
3 files changed, 43 insertions(+), 39 deletions(-)
diff --git a/java/org/apache/catalina/ssi/SSIMediator.java b/java/org/apache/catalina/ssi/SSIMediator.java
index bac7bb4a48..7f82a64a49 100644
--- a/java/org/apache/catalina/ssi/SSIMediator.java
+++ b/java/org/apache/catalina/ssi/SSIMediator.java
@@ -20,8 +20,10 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Date;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
@@ -100,6 +102,12 @@ public class SSIMediator {
* Number of regex match groups from the last match operation.
*/
protected int lastMatchCount = 0;
+ /**
+ * Built-in variables supplied by this mediator, keyed by upper-case name. These are held
+ * locally rather than in the external resolver since the resolver rejects names under the
+ * reserved prefixes, which included the names historically used for these variables.
+ */
+ private final Map<String, String> builtinVariables = new HashMap<>();
/**
@@ -111,7 +119,7 @@ public class SSIMediator {
public SSIMediator(SSIExternalResolver ssiExternalResolver, long lastModifiedDate) {
this.ssiExternalResolver = ssiExternalResolver;
this.lastModifiedDate = lastModifiedDate;
- setConfigTimeFmt(DEFAULT_CONFIG_TIME_FMT, true);
+ setConfigTimeFmt(DEFAULT_CONFIG_TIME_FMT);
}
@@ -131,24 +139,13 @@ public class SSIMediator {
* @param configTimeFmt the time format string
*/
public void setConfigTimeFmt(String configTimeFmt) {
- setConfigTimeFmt(configTimeFmt, false);
- }
-
-
- /**
- * Sets the time format string and updates date variables accordingly.
- *
- * @param configTimeFmt the time format string
- * @param fromConstructor true if called from the constructor
- */
- public void setConfigTimeFmt(String configTimeFmt, boolean fromConstructor) {
this.configTimeFmt = configTimeFmt;
this.strftime = new Strftime(configTimeFmt, Locale.US);
/*
* Variables like DATE_LOCAL, DATE_GMT, and LAST_MODIFIED need to be updated when the timefmt changes. This is
* what Apache SSI does.
*/
- setDateVariables(fromConstructor);
+ setDateVariables();
}
@@ -308,8 +305,7 @@ public class SSIMediator {
// Try getting it externally first, if it fails, try getting the 'built-in' value
variableValue = ssiExternalResolver.getVariableValue(variableName);
if (variableValue == null) {
- variableName = variableName.toUpperCase(Locale.ENGLISH);
- variableValue = ssiExternalResolver.getVariableValue(className + "." + variableName);
+ variableValue = builtinVariables.get(variableName.toUpperCase(Locale.ENGLISH));
}
if (variableValue != null) {
variableValue = encode(variableValue, encoding);
@@ -517,30 +513,23 @@ public class SSIMediator {
/**
* Updates the built-in date variables (DATE_GMT, DATE_LOCAL, LAST_MODIFIED).
- *
- * @param fromConstructor true if called from the constructor
- */
- protected void setDateVariables(boolean fromConstructor) {
- boolean alreadySet = ssiExternalResolver.getVariableValue(className + ".alreadyset") != null;
- // skip this if we are being called from the constructor, and this has already been set
- if (!(fromConstructor && alreadySet)) {
- ssiExternalResolver.setVariableValue(className + ".alreadyset", "true");
- Date date = new Date();
- TimeZone timeZone = TimeZone.getTimeZone("GMT");
- String retVal = formatDate(date, timeZone);
- /*
- * If we are setting on of the date variables, we want to remove them from the user defined list of
- * variables, because this is what Apache does.
- */
- setVariableValue("DATE_GMT", null);
- ssiExternalResolver.setVariableValue(className + ".DATE_GMT", retVal);
- retVal = formatDate(date, null);
- setVariableValue("DATE_LOCAL", null);
- ssiExternalResolver.setVariableValue(className + ".DATE_LOCAL", retVal);
- retVal = formatDate(new Date(lastModifiedDate), null);
- setVariableValue("LAST_MODIFIED", null);
- ssiExternalResolver.setVariableValue(className + ".LAST_MODIFIED", retVal);
- }
+ */
+ protected void setDateVariables() {
+ Date date = new Date();
+ TimeZone timeZone = TimeZone.getTimeZone("GMT");
+ String retVal = formatDate(date, timeZone);
+ /*
+ * If we are setting on of the date variables, we want to remove them from the user defined list of variables,
+ * because this is what Apache does.
+ */
+ setVariableValue("DATE_GMT", null);
+ builtinVariables.put("DATE_GMT", retVal);
+ retVal = formatDate(date, null);
+ setVariableValue("DATE_LOCAL", null);
+ builtinVariables.put("DATE_LOCAL", retVal);
+ retVal = formatDate(new Date(lastModifiedDate), null);
+ setVariableValue("LAST_MODIFIED", null);
+ builtinVariables.put("LAST_MODIFIED", retVal);
}
diff --git a/test/org/apache/catalina/ssi/TestSsiServlet.java b/test/org/apache/catalina/ssi/TestSsiServlet.java
index 009caa5f3c..28e8fa6337 100644
--- a/test/org/apache/catalina/ssi/TestSsiServlet.java
+++ b/test/org/apache/catalina/ssi/TestSsiServlet.java
@@ -67,6 +67,9 @@ public class TestSsiServlet extends TomcatBaseTest {
Assert.assertTrue(body.contains("path is relative"));
Assert.assertTrue(body.contains("1k"));
Assert.assertTrue(body.contains("SERVER_PROTOCOL"));
+ // Any undefined variable renders as "(none)". The page only echoes
+ // variables that must be defined, including the built-in date variables.
+ Assert.assertFalse(body.contains("(none)"));
}
}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 4975384ef6..c98f19f0cd 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -1915,6 +1915,18 @@
evaluation of existing documents using unparenthesized mixed
<code>&&</code> and <code>||</code> chains. (remm)
</fix>
+ <fix>
+ Fix the built-in SSI variables <code>DATE_GMT</code>,
+ <code>DATE_LOCAL</code> and <code>LAST_MODIFIED</code> always
+ resolving to <code>null</code> and rendering as
+ <code>(none)</code>. The mediator stored and looked them up as
+ external variables using names starting with its own fully qualified
+ class name, which the servlet resolver rejects since those names fall
+ under the reserved <code>org.apache.catalina.</code> prefix. Hold the
+ built-in variables in the mediator itself and resolve them there, as
+ they are per-document values that do not need to be stored in the
+ external resolver. (remm)
+ </fix>
</changelog>
</subsection>
<subsection name="Coyote">
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 19/23: Remove the incomplete WAR left behind when copying a WAR to the appBase fails.
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit fd3fbc7cbfdd4c7d05823e0935601d811f569385
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 16:57:59 2026 +0200
Remove the incomplete WAR left behind when copying a WAR to the appBase
fails.
addWebapp(String, URL) creates the target WAR in the host's appBase before
streaming the source into it. If the copy failed part way through, the
truncated WAR remained in the appBase and the method's own conflict check
then rejected every subsequent attempt to deploy to the same context path
with an IllegalArgumentException, so a transient I/O error turned into a
permanent failure until the file was deleted manually.
Delete the incomplete WAR when the copy throws an IOException before
rethrowing the original exception. If the file cannot be deleted, log a
warning naming its location so the administrator knows what to remove.
---
java/org/apache/catalina/startup/LocalStrings.properties | 1 +
java/org/apache/catalina/startup/Tomcat.java | 10 ++++++++++
2 files changed, 11 insertions(+)
diff --git a/java/org/apache/catalina/startup/LocalStrings.properties b/java/org/apache/catalina/startup/LocalStrings.properties
index d19bc76a42..a33e78196c 100644
--- a/java/org/apache/catalina/startup/LocalStrings.properties
+++ b/java/org/apache/catalina/startup/LocalStrings.properties
@@ -172,6 +172,7 @@ setNextNamingRule.invalidParent=Parent should be either a Context or a NamingRes
tomcat.addWebapp.conflictChild=Unable to deploy WAR at [{0}] to context path [{1}] because of existing context [{2}]
tomcat.addWebapp.conflictFile=Unable to deploy WAR at [{0}] to context path [{1}] because of existing file [{2}]
+tomcat.addWebapp.copyCleanupFailed=Unable to deploy WAR at [{0}] to context path [{1}]. The incomplete copy at [{2}] could not be deleted. Remove that file manually before attempting to deploy to this context path again.
tomcat.baseDirMakeFail=Unable to create the directory [{0}] to use as the base directory
tomcat.baseDirNotDir=The location [{0}] specified for the base directory is not a directory
tomcat.defaultMimeTypeMappingsFail=Unable to load the default MIME types
diff --git a/java/org/apache/catalina/startup/Tomcat.java b/java/org/apache/catalina/startup/Tomcat.java
index fadc49f262..af44e397b6 100644
--- a/java/org/apache/catalina/startup/Tomcat.java
+++ b/java/org/apache/catalina/startup/Tomcat.java
@@ -276,6 +276,16 @@ public class Tomcat {
try (InputStream is = uConn.getInputStream(); OutputStream os = new FileOutputStream(targetWar)) {
IOTools.flow(is, os);
+ } catch (IOException e) {
+ // An incomplete WAR left in the appBase would block any subsequent attempt to deploy
+ // to this context path via the conflict checks above, so remove it before rethrowing.
+ if (targetWar.exists() && !targetWar.delete()) {
+ Logger.getLogger(getLoggerName(getHost(), contextPath)).log(Level.WARNING,
+ sm.getString("tomcat.addWebapp.copyCleanupFailed", source, contextPath,
+ targetWar.getAbsolutePath()),
+ e);
+ }
+ throw e;
}
return addWebapp(contextPath, targetWar.getAbsolutePath());
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 10/23: Require both var and value attributes for the SSI #set directive
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 68788f48ff53e70aac88544e428a76335c083365
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 14:56:59 2026 +0200
Require both var and value attributes for the SSI #set directive
Apache mod_include requires both the var and the value attribute for
the #set directive, in any order. Tomcat already collects the
attributes before validating them so any order is accepted, but a
missing value attribute was silently ignored: no variable was set, no
message was logged and processing continued, while a missing var
attribute produced the configured error message and stopped
processing. The same logical error was therefore handled two
different ways depending on which attribute was omitted.
Validation is now symmetric: a missing value attribute is logged via
the new ssiSet.noValue message, the configured error message is
written to the response and SSIStopProcessingException is thrown,
mirroring the existing missing-var and invalid-attribute paths. An
empty value attribute (value="") continues to work, since the parsed
parameter value is an empty string rather than null.
Update TestSSISet.testVarOnly to assert the new error behaviour.
---
.../apache/catalina/ssi/LocalStrings.properties | 1 +
java/org/apache/catalina/ssi/SSISet.java | 24 +++++++++++++---------
test/org/apache/catalina/ssi/TestSSISet.java | 4 ++--
3 files changed, 17 insertions(+), 12 deletions(-)
diff --git a/java/org/apache/catalina/ssi/LocalStrings.properties b/java/org/apache/catalina/ssi/LocalStrings.properties
index 0fbbc0d9e4..5cb718e6bd 100644
--- a/java/org/apache/catalina/ssi/LocalStrings.properties
+++ b/java/org/apache/catalina/ssi/LocalStrings.properties
@@ -49,4 +49,5 @@ ssiServletExternalResolver.normalizationError=Normalization returned null for pa
ssiServletExternalResolver.removeFilenameError=Cannot remove filename from path [{0}]
ssiServletExternalResolver.requestDispatcherError=Cannot get request dispatcher for path [{0}]
+ssiSet.noValue=No value specified
ssiSet.noVariable=No variable specified
diff --git a/java/org/apache/catalina/ssi/SSISet.java b/java/org/apache/catalina/ssi/SSISet.java
index 5b364d82c0..b21b303520 100644
--- a/java/org/apache/catalina/ssi/SSISet.java
+++ b/java/org/apache/catalina/ssi/SSISet.java
@@ -69,17 +69,21 @@ public class SSISet implements SSICommand {
throw new SSIStopProcessingException();
}
}
+ // Both attributes are required, as Apache requires, but they may be
+ // given in any order
if (variableName == null) {
- if (variableValue != null) {
- ssiMediator.log(sm.getString("ssiSet.noVariable"));
- writer.write(errorMessage);
- throw new SSIStopProcessingException();
- }
- } else if (variableValue != null) {
- String substitutedValue = ssiMediator.substituteVariables(variableValue);
- ssiMediator.setVariableValue(variableName, substitutedValue);
- lastModified = System.currentTimeMillis();
+ ssiMediator.log(sm.getString("ssiSet.noVariable"));
+ writer.write(errorMessage);
+ throw new SSIStopProcessingException();
+ }
+ if (variableValue == null) {
+ ssiMediator.log(sm.getString("ssiSet.noValue"));
+ writer.write(errorMessage);
+ throw new SSIStopProcessingException();
}
+ String substitutedValue = ssiMediator.substituteVariables(variableValue);
+ ssiMediator.setVariableValue(variableName, substitutedValue);
+ lastModified = System.currentTimeMillis();
return lastModified;
}
-}
\ No newline at end of file
+}
diff --git a/test/org/apache/catalina/ssi/TestSSISet.java b/test/org/apache/catalina/ssi/TestSSISet.java
index 77e351bf3f..300c1fc017 100644
--- a/test/org/apache/catalina/ssi/TestSSISet.java
+++ b/test/org/apache/catalina/ssi/TestSSISet.java
@@ -49,10 +49,10 @@ public class TestSSISet {
@Test
public void testVarOnly() throws Exception {
- // Setting only the variable name does nothing and is not an error
+ // The value attribute is required, as it is for Apache
SSIMediator mediator = newMediator();
String output = process(mediator, new String[] { "var" }, new String[] { "foo" });
- Assert.assertEquals("", output);
+ Assert.assertEquals(mediator.getConfigErrMsg(), output);
Assert.assertNull(mediator.getVariableValue("foo"));
}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 13/23: Fall back to classpath defaults when catalina.properties is corrupt
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit ae2d22b8da130c8d478a12cfc36b962d0f309ab1
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 15:32:02 2026 +0200
Fall back to classpath defaults when catalina.properties is corrupt
loadProperties() assigned the fresh Properties object to the static
field before loading it, so a parse failure left the field pointing at
a partially populated (or empty) object. Because the stream had been
opened successfully, both the fallback to the default properties
shipped in the class path and the failure warning were skipped. The
common, server and shared loader settings were then missing, Bootstrap
created class loaders without the configured entries and Tomcat failed
with a confusing ClassNotFoundException for the Catalina class instead
of anything pointing at the corrupt file.
Loading now happens into a local object that is only published once
the whole stream has been parsed. When the configured source is
missing or cannot be parsed, the defaults from the class path are
loaded instead; only when even that fails is the warning logged and an
empty set used. This also keeps the partially parsed values from being
registered as system properties.
---
.../catalina/startup/CatalinaProperties.java | 57 +++++++++++++++-------
1 file changed, 40 insertions(+), 17 deletions(-)
diff --git a/java/org/apache/catalina/startup/CatalinaProperties.java b/java/org/apache/catalina/startup/CatalinaProperties.java
index 49bafb2d13..04e975c8ca 100644
--- a/java/org/apache/catalina/startup/CatalinaProperties.java
+++ b/java/org/apache/catalina/startup/CatalinaProperties.java
@@ -95,31 +95,24 @@ public class CatalinaProperties {
}
}
- if (is == null) {
- try {
- is = CatalinaProperties.class.getResourceAsStream("/org/apache/catalina/startup/catalina.properties");
- } catch (Throwable t) {
- handleThrowable(t);
- }
+ if (is != null) {
+ properties = load(is);
}
- if (is != null) {
+ if (properties == null) {
+ // The configured file could not be opened or could not be parsed.
+ // Fall back to the defaults from the class path.
try {
- properties = new Properties();
- properties.load(is);
+ is = CatalinaProperties.class.getResourceAsStream("/org/apache/catalina/startup/catalina.properties");
} catch (Throwable t) {
handleThrowable(t);
- log.warn(t.getMessage(), t);
- } finally {
- try {
- is.close();
- } catch (IOException ioe) {
- log.warn("Could not close catalina properties file", ioe);
- }
+ }
+ if (is != null) {
+ properties = load(is);
}
}
- if ((is == null)) {
+ if (properties == null) {
log.warn("Failed to load catalina properties file");
// That's fine - we have reasonable defaults.
properties = new Properties();
@@ -137,6 +130,36 @@ public class CatalinaProperties {
}
+ /**
+ * Loads properties from the specified stream, closing the stream when done.
+ *
+ * @param is The stream to read
+ *
+ * @return the loaded properties, or {@code null} if the stream could not
+ * be parsed completely
+ */
+ private static Properties load(InputStream is) {
+ try {
+ Properties loaded = new Properties();
+ loaded.load(is);
+ // Publish the properties only once the whole stream has been
+ // parsed, so a corrupt file cannot silently replace the defaults
+ // with partially parsed values.
+ return loaded;
+ } catch (Throwable t) {
+ handleThrowable(t);
+ log.warn(t.getMessage(), t);
+ return null;
+ } finally {
+ try {
+ is.close();
+ } catch (IOException ioe) {
+ log.warn("Could not close catalina properties file", ioe);
+ }
+ }
+ }
+
+
private static boolean isSchemeChar(char c) {
return Character.isLetterOrDigit(c) || c == '+' || c == '-' || c == '.';
}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 07/23: Make SSI #fsize output independent of the JVM default locale
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 32cf81e2f2dd998f6db096b4a971c7286a60d6c9
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 14:21:57 2026 +0200
Make SSI #fsize output independent of the JVM default locale
The DecimalFormat instances used by the abbreviated and bytes size
formats were created with the JVM default locale, so server-generated
#fsize output varied with the locale, for example a 1234 byte file
rendered as 1.234 rather than 1,234 on a German JVM and 1.5M rendered
with a comma decimal separator.
Build the formats with the symbols of the root locale so the output is
locale independent, consistent with the httpd behavior this code mimics.
Add a unit test that checks the formatting under a non-default locale.
---
java/org/apache/catalina/ssi/SSIFsize.java | 7 +-
test/org/apache/catalina/ssi/TestSSIFsize.java | 105 +++++++++++++++++++++++++
2 files changed, 110 insertions(+), 2 deletions(-)
diff --git a/java/org/apache/catalina/ssi/SSIFsize.java b/java/org/apache/catalina/ssi/SSIFsize.java
index 85a9647c34..bf3ed42a4e 100644
--- a/java/org/apache/catalina/ssi/SSIFsize.java
+++ b/java/org/apache/catalina/ssi/SSIFsize.java
@@ -20,6 +20,8 @@ package org.apache.catalina.ssi;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.Locale;
import org.apache.tomcat.util.res.StringManager;
@@ -97,10 +99,11 @@ public final class SSIFsize implements SSICommand {
// We try to mimic httpd here, as we do everywhere.
// All the 'magic' numbers are from the util_script.c httpd source file.
// Should use KiB and MiB in output but use k and M for consistency with httpd.
+ // The formatting is locale independent since it is server generated output.
private String formatSize(long size, String format) {
String retString;
if (format.equalsIgnoreCase("bytes")) {
- DecimalFormat decimalFormat = new DecimalFormat("#,##0");
+ DecimalFormat decimalFormat = new DecimalFormat("#,##0", DecimalFormatSymbols.getInstance(Locale.ROOT));
retString = decimalFormat.format(size);
} else {
if (size < 0) {
@@ -113,7 +116,7 @@ public final class SSIFsize implements SSICommand {
retString = Long.toString((size + 512) / ONE_KIBIBYTE);
retString += "k";
} else if (size < 99 * ONE_MEBIBYTE) {
- DecimalFormat decimalFormat = new DecimalFormat("0.0M");
+ DecimalFormat decimalFormat = new DecimalFormat("0.0M", DecimalFormatSymbols.getInstance(Locale.ROOT));
retString = decimalFormat.format(size / (double) ONE_MEBIBYTE);
} else {
retString = Long.toString((size + (529 * ONE_KIBIBYTE)) / ONE_MEBIBYTE);
diff --git a/test/org/apache/catalina/ssi/TestSSIFsize.java b/test/org/apache/catalina/ssi/TestSSIFsize.java
new file mode 100644
index 0000000000..bd7e33d9ed
--- /dev/null
+++ b/test/org/apache/catalina/ssi/TestSSIFsize.java
@@ -0,0 +1,105 @@
+/*
+ * 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.catalina.ssi;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Locale;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestSSIFsize {
+
+ @Test
+ public void testFormatSizeIsLocaleIndependent() throws Exception {
+ Locale defaultLocale = Locale.getDefault();
+ try {
+ Locale.setDefault(Locale.GERMANY);
+ Assert.assertEquals("1,234", formatSize(1234, "bytes"));
+ Assert.assertEquals(" 1.5M", formatSize(1572864, "abbrev"));
+ } finally {
+ Locale.setDefault(defaultLocale);
+ }
+ }
+
+
+ private String formatSize(long size, String format) throws IOException {
+ SSIMediator mediator = new SSIMediator(new TesterSSIExternalResolver(size), 0);
+ mediator.setConfigSizeFmt(format);
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter writer = new PrintWriter(stringWriter);
+ new SSIFsize().process(mediator, "fsize", new String[] { "file" }, new String[] { "any" }, writer);
+ writer.flush();
+ return stringWriter.toString();
+ }
+
+ /**
+ * Minimal implementation that provides the bare essentials required for the unit tests.
+ */
+ private static class TesterSSIExternalResolver implements SSIExternalResolver {
+
+ private final long fileSize;
+
+ TesterSSIExternalResolver(long fileSize) {
+ this.fileSize = fileSize;
+ }
+
+ @Override
+ public void addVariableNames(Collection<String> variableNames) {
+ // NO-OP
+ }
+
+ @Override
+ public String getVariableValue(String name) {
+ return null;
+ }
+
+ @Override
+ public void setVariableValue(String name, String value) {
+ // NO-OP
+ }
+
+ @Override
+ public Date getCurrentDate() {
+ return null;
+ }
+
+ @Override
+ public long getFileSize(String path, boolean virtual) {
+ return fileSize;
+ }
+
+ @Override
+ public long getFileLastModified(String path, boolean virtual) {
+ return 0;
+ }
+
+ @Override
+ public String getFileText(String path, boolean virtual) {
+ return null;
+ }
+
+ @Override
+ public void log(String message, Throwable throwable) {
+ // NO-OP
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 22/23: Report service provider loading failures caused by a LinkageError via the documented IOException contract.
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 172dc23438c8b841683c1fa452df3cbe957568bd
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 17:31:20 2026 +0200
Report service provider loading failures caused by a LinkageError via the
documented IOException contract.
WebappServiceLoader.loadServices() loads each provider with
Class.forName(name, true, loader), which initializes the class. A defective
provider can therefore fail with a LinkageError, for example a
NoClassDefFoundError caused by a missing transitive dependency, an
ExceptionInInitializerError caused by a failing static initialiser or a
ClassFormatError caused by a corrupt class file. None of these were caught.
The documented contract of load() is to report any problem loading a service
as an IOException and its only caller,
ContextConfig.processServletContainerInitializers(), only handles that
exception. An escaped Error bypassed the clean "the ServletContainerInitializers
for this application could not be loaded" failure path and aborted context
configuration via the generic Throwable handling in LifecycleBase.
Catch LinkageError alongside the existing exceptions and wrap it in an
IOException so the failure follows the documented contract and the existing
controlled context failure path. A test using a provider whose static
initialiser fails verifies the behaviour.
---
.../catalina/startup/WebappServiceLoader.java | 5 +++
.../catalina/startup/TestWebappServiceLoader.java | 20 ++++++++++++
.../TesterServiceWithFailingInitializer.java | 38 ++++++++++++++++++++++
3 files changed, 63 insertions(+)
diff --git a/java/org/apache/catalina/startup/WebappServiceLoader.java b/java/org/apache/catalina/startup/WebappServiceLoader.java
index bb8f60c000..baaaf35ce6 100644
--- a/java/org/apache/catalina/startup/WebappServiceLoader.java
+++ b/java/org/apache/catalina/startup/WebappServiceLoader.java
@@ -232,6 +232,11 @@ public class WebappServiceLoader<T> {
services.add(serviceType.cast(clazz.getConstructor().newInstance()));
} catch (ReflectiveOperationException | ClassCastException e) {
throw new IOException(e);
+ } catch (LinkageError e) {
+ // Loading with initialization enabled can fail with an Error, for example a missing
+ // transitive dependency or a failing static initializer. Report those via the
+ // documented IOException contract rather than letting the Error escape.
+ throw new IOException(e);
}
}
return Collections.unmodifiableList(services);
diff --git a/test/org/apache/catalina/startup/TestWebappServiceLoader.java b/test/org/apache/catalina/startup/TestWebappServiceLoader.java
index e7fe0e7269..cc3c2356c7 100644
--- a/test/org/apache/catalina/startup/TestWebappServiceLoader.java
+++ b/test/org/apache/catalina/startup/TestWebappServiceLoader.java
@@ -172,6 +172,26 @@ public class TestWebappServiceLoader {
}
}
+ @Test
+ public void testServiceInitializationFailsWithLinkageError() throws Exception {
+ Class<?> sci = TesterServiceWithFailingInitializer.class;
+ loader = new WebappServiceLoader<>(context);
+ cl.loadClass(sci.getName());
+ EasyMock.expectLastCall()
+ .andReturn(sci);
+ LinkedHashSet<String> names = new LinkedHashSet<>();
+ names.add(sci.getName());
+ control.replay();
+ try {
+ loader.loadServices(ServletContainerInitializer.class, names);
+ Assert.fail("Expected IOException");
+ } catch (IOException ioe) {
+ assertThat(ioe.getCause(), instanceOf(LinkageError.class));
+ } finally {
+ control.verify();
+ }
+ }
+
@Test
public void testServiceCannotBeConstructed() throws Exception {
Class<?> sci = Integer.class;
diff --git a/test/org/apache/catalina/startup/TesterServiceWithFailingInitializer.java b/test/org/apache/catalina/startup/TesterServiceWithFailingInitializer.java
new file mode 100644
index 0000000000..a99f3a945c
--- /dev/null
+++ b/test/org/apache/catalina/startup/TesterServiceWithFailingInitializer.java
@@ -0,0 +1,38 @@
+/*
+ * 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.catalina.startup;
+
+import java.util.Set;
+
+import jakarta.servlet.ServletContainerInitializer;
+import jakarta.servlet.ServletContext;
+
+public class TesterServiceWithFailingInitializer implements ServletContainerInitializer {
+
+ static {
+ // Force an ExceptionInInitializerError (a LinkageError) when this
+ // class is initialized by Class.forName(name, true, loader).
+ if (Boolean.TRUE) {
+ throw new IllegalStateException("Test failure in static initializer");
+ }
+ }
+
+ @Override
+ public void onStartup(Set<Class<?>> c, ServletContext ctx) {
+ // Not reached
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 20/23: Correct the Tomcat.init() Javadoc regarding the configuration source.
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 09bcd34a7b4adfe926be16d0e133eec836886f09
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 17:03:00 2026 +0200
Correct the Tomcat.init() Javadoc regarding the configuration source.
The Javadoc implied that calling init() with no configuration source would
always use the default configuration locations. In fact the configuration
source is a JVM-global value managed by ConfigFileLoader.setSource(), which is
deliberately "only the first call has an effect": once a source has been
configured, by an earlier Tomcat instance or any other caller in the same JVM,
it is used for all subsequent configuration and can be neither replaced by a
different source nor reset to the defaults by passing null.
No behaviour change. This only documents the existing, intentional semantics so
that the default locations are correctly described as applying only when no
source has been configured previously in the JVM.
---
java/org/apache/catalina/startup/Tomcat.java | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/java/org/apache/catalina/startup/Tomcat.java b/java/org/apache/catalina/startup/Tomcat.java
index af44e397b6..e071b4a749 100644
--- a/java/org/apache/catalina/startup/Tomcat.java
+++ b/java/org/apache/catalina/startup/Tomcat.java
@@ -408,8 +408,11 @@ public class Tomcat {
/**
* Initialize the server given the specified configuration source. The server will be loaded according to the Tomcat
- * configuration files contained in the source (server.xml, web.xml, context.xml, SSL certificates, etc). If no
- * configuration source is specified, it will use the default locations for these files.
+ * configuration files contained in the source (server.xml, web.xml, context.xml, SSL certificates, etc). The
+ * configuration source is global to the JVM. Once a source has been configured, by this instance or by any earlier
+ * use in the same JVM, it will be used for all subsequent configuration and cannot be replaced: neither a later
+ * call specifying a different source nor a call specifying no source has any effect. If no source is specified and
+ * none has been configured previously, the default locations for these files are used.
*
* @param source The configuration source
*/
@@ -419,8 +422,11 @@ public class Tomcat {
/**
* Initialize the server given the specified configuration source. The server will be loaded according to the Tomcat
- * configuration files contained in the source (server.xml, web.xml, context.xml, SSL certificates, etc). If no
- * configuration source is specified, it will use the default locations for these files.
+ * configuration files contained in the source (server.xml, web.xml, context.xml, SSL certificates, etc). The
+ * configuration source is global to the JVM. Once a source has been configured, by this instance or by any earlier
+ * use in the same JVM, it will be used for all subsequent configuration and cannot be replaced: neither a later
+ * call specifying a different source nor a call specifying no source has any effect. If no source is specified and
+ * none has been configured previously, the default locations for these files are used.
*
* @param source The configuration source
* @param catalinaArguments The arguments that should be passed to Catalina
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 05/23: Report a parse error for unterminated literals in SSI expressions
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 214ee2d34598d4d0f52684e5a2cb77b13b0f85a7
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 14:08:04 2026 +0200
Report a parse error for unterminated literals in SSI expressions
An unterminated quoted string in an SSI conditional expression was
silently accepted: the scan ran to the end of the expression and the
remainder of the expression became the literal value, silently changing
the semantics of the expression compared to what the document author
intended.
Signal this with a new TOKEN_ERROR token type when the closing quote is
missing, and convert it into a ParseException in ExpressionParseTree so
the failure takes the existing parse error path, stopping the processing
of the document at the failing directive. Apply the same treatment to an
unterminated regular expression for consistency, replacing the previous
remainder-as-literal behavior.
---
java/org/apache/catalina/ssi/ExpressionParseTree.java | 3 +++
java/org/apache/catalina/ssi/ExpressionTokenizer.java | 15 ++++++++++++---
java/org/apache/catalina/ssi/LocalStrings.properties | 1 +
.../apache/catalina/ssi/TestExpressionParseTree.java | 17 +++++++++++++++++
.../apache/catalina/ssi/TestExpressionTokenizer.java | 14 +++++++++++---
5 files changed, 44 insertions(+), 6 deletions(-)
diff --git a/java/org/apache/catalina/ssi/ExpressionParseTree.java b/java/org/apache/catalina/ssi/ExpressionParseTree.java
index d18cb7b948..17162d37fe 100644
--- a/java/org/apache/catalina/ssi/ExpressionParseTree.java
+++ b/java/org/apache/catalina/ssi/ExpressionParseTree.java
@@ -241,6 +241,9 @@ public class ExpressionParseTree {
break;
case ExpressionTokenizer.TOKEN_END:
break;
+ case ExpressionTokenizer.TOKEN_ERROR:
+ throw new ParseException(sm.getString("expressionParseTree.unterminatedLiteral"),
+ et.getIndex());
}
}
// Finish off the rest of the opps
diff --git a/java/org/apache/catalina/ssi/ExpressionTokenizer.java b/java/org/apache/catalina/ssi/ExpressionTokenizer.java
index 825b82c30a..981a85fe4b 100644
--- a/java/org/apache/catalina/ssi/ExpressionTokenizer.java
+++ b/java/org/apache/catalina/ssi/ExpressionTokenizer.java
@@ -48,6 +48,8 @@ public class ExpressionTokenizer {
public static final int TOKEN_LT = 11;
/** Token type indicating end of expression. */
public static final int TOKEN_END = 12;
+ /** Token type indicating an unterminated string or regular expression. */
+ public static final int TOKEN_ERROR = 13;
private final char[] expr;
private String tokenVal = null;
private int index;
@@ -176,6 +178,10 @@ public class ExpressionTokenizer {
}
escaped = false;
}
+ if (index == length) {
+ // The closing quote is missing
+ return TOKEN_ERROR;
+ }
end = index;
index++; // Skip the end quote
} else if (currentChar == '/') {
@@ -191,9 +197,12 @@ public class ExpressionTokenizer {
}
escaped = false;
}
- // If the regular expression was not terminated, the token is the
- // remainder of the expression, otherwise it includes the closing slash
- end = Math.min(++index, length);
+ if (index == length) {
+ // The closing slash is missing
+ return TOKEN_ERROR;
+ }
+ // The token includes the closing slash
+ end = ++index;
} else {
// End is the next whitespace character
for (; index < length; index++) {
diff --git a/java/org/apache/catalina/ssi/LocalStrings.properties b/java/org/apache/catalina/ssi/LocalStrings.properties
index 5cebaec4d0..0fbbc0d9e4 100644
--- a/java/org/apache/catalina/ssi/LocalStrings.properties
+++ b/java/org/apache/catalina/ssi/LocalStrings.properties
@@ -20,6 +20,7 @@ expressionParseTree.extraNodes=Extra nodes created
expressionParseTree.invalidExpression=Invalid expression [{0}]
expressionParseTree.missingOperand=Missing operand
expressionParseTree.noNodes=No nodes created
+expressionParseTree.unterminatedLiteral=Unterminated string or regular expression
expressionParseTree.unusedOpCodes=Unused nodes exist
ssiCommand.invalidAttribute=Invalid attribute [{0}]
diff --git a/test/org/apache/catalina/ssi/TestExpressionParseTree.java b/test/org/apache/catalina/ssi/TestExpressionParseTree.java
index 4865305a3c..0a3cbd5bf5 100644
--- a/test/org/apache/catalina/ssi/TestExpressionParseTree.java
+++ b/test/org/apache/catalina/ssi/TestExpressionParseTree.java
@@ -156,6 +156,23 @@ public class TestExpressionParseTree {
}
+ @Test
+ public void testUnterminatedLiteral() throws Exception {
+ // Unterminated quoted strings and regular expressions must produce a
+ // parse error rather than silently changing the expression semantics
+ String[] expressions = { "a = \"x", "a = 'x", "a = /x", "\"", "/", "'x = y" };
+ for (String expression : expressions) {
+ SSIMediator mediator = new SSIMediator(new TesterSSIExternalResolver(), LAST_MODIFIED);
+ try {
+ new ExpressionParseTree(expression, mediator);
+ Assert.fail("Expected a parse error for [" + expression + "]");
+ } catch (ParseException pe) {
+ // Expected
+ }
+ }
+ }
+
+
@Test
public void testMissingOperand() throws Exception {
// Operators missing an operand must produce a parse error rather than
diff --git a/test/org/apache/catalina/ssi/TestExpressionTokenizer.java b/test/org/apache/catalina/ssi/TestExpressionTokenizer.java
index e4f78cdb55..85bd512612 100644
--- a/test/org/apache/catalina/ssi/TestExpressionTokenizer.java
+++ b/test/org/apache/catalina/ssi/TestExpressionTokenizer.java
@@ -101,11 +101,19 @@ public class TestExpressionTokenizer {
// Escaped slash inside a regular expression
parameterSets.add(new Object[] { "/a\\/b/",
new int[] { ExpressionTokenizer.TOKEN_STRING }, new String[] { "/a\\/b/" } });
- // Unterminated regular expression: the token is the remainder
+ // Unterminated regular expression yields an error token
parameterSets.add(new Object[] { "a = /x",
new int[] { ExpressionTokenizer.TOKEN_STRING, ExpressionTokenizer.TOKEN_EQ,
- ExpressionTokenizer.TOKEN_STRING },
- new String[] { "a", null, "/x" } });
+ ExpressionTokenizer.TOKEN_ERROR },
+ new String[] { "a", null, null } });
+
+ // Unterminated quoted strings yield an error token
+ parameterSets.add(new Object[] { "a = \"x",
+ new int[] { ExpressionTokenizer.TOKEN_STRING, ExpressionTokenizer.TOKEN_EQ,
+ ExpressionTokenizer.TOKEN_ERROR },
+ new String[] { "a", null, null } });
+ parameterSets.add(new Object[] { "'x",
+ new int[] { ExpressionTokenizer.TOKEN_ERROR }, new String[] { null } });
// Operators mixed with strings
parameterSets.add(new Object[] { "a && b",
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 04/23: Restore the interrupt status when SSI #exec cmd is interrupted
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit c8664c6796be864dc67b2b933dceb6782eb4e80c
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 14:02:13 2026 +0200
Restore the interrupt status when SSI #exec cmd is interrupted
The InterruptedException thrown by Process.waitFor() in the SSI
"#exec cmd" handler was caught, logged and reported via the
configurable error message, but the interrupt status of the request
thread was silently discarded. Restore it by calling
Thread.currentThread().interrupt() in the handler so that the enclosing
container thread can continue its shutdown processing.
---
java/org/apache/catalina/ssi/SSIExec.java | 3 +++
1 file changed, 3 insertions(+)
diff --git a/java/org/apache/catalina/ssi/SSIExec.java b/java/org/apache/catalina/ssi/SSIExec.java
index 9090ece48b..fa4e1210d6 100644
--- a/java/org/apache/catalina/ssi/SSIExec.java
+++ b/java/org/apache/catalina/ssi/SSIExec.java
@@ -111,6 +111,9 @@ public class SSIExec implements SSICommand {
}
lastModified = System.currentTimeMillis();
} catch (InterruptedException e) {
+ // Restore the interrupt status so that the enclosing container
+ // thread can continue its shutdown processing
+ Thread.currentThread().interrupt();
ssiMediator.log(sm.getString("ssiExec.executeFailed", substitutedValue), e);
writer.write(configErrMsg);
} catch (IOException ioe) {
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 17/23: Restore the previous web application when legacy application migration cannot place the migrated application.
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 37ccf9feb6b4b27a0bd0fcf57ddb6f2ca93dd3b0
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 16:43:59 2026 +0200
Restore the previous web application when legacy application migration
cannot place the migrated application.
migrateLegacyApp() moves the previous version of the application from the
deployment location to a temporary location before moving the newly migrated
application into place. Previously, if that final move failed, the original
code left the previous application stranded in the temporary location: it was
neither restored to the deployment location nor cleaned up, so the
application disappeared from the deployment location and the stranded copy
was never referenced again.
The completion of each of the two moves is now tracked explicitly. When the
previous application has been moved aside but the migrated application cannot
be placed, the previous application is restored to the deployment location and
the event is logged. If restoring it also fails, the copy is deliberately
retained for manual recovery and a warning naming both locations is logged,
rather than risking deletion of the only remaining copy. The previous
application copy is only discarded once the migrated application is confirmed
to be in place.
---
java/org/apache/catalina/startup/HostConfig.java | 55 +++++++++++++++++++++-
.../catalina/startup/LocalStrings.properties | 2 +
webapps/docs/changelog.xml | 11 +++++
3 files changed, 67 insertions(+), 1 deletion(-)
diff --git a/java/org/apache/catalina/startup/HostConfig.java b/java/org/apache/catalina/startup/HostConfig.java
index 0e367fecc0..e2ddaf68a1 100644
--- a/java/org/apache/catalina/startup/HostConfig.java
+++ b/java/org/apache/catalina/startup/HostConfig.java
@@ -1195,7 +1195,13 @@ public class HostConfig implements LifecycleListener {
*/
protected void migrateLegacyApp(File source, File destination) {
File tempNew = null;
- File tempOld;
+ File tempOld = null;
+ // Track which moves completed so cleanup can decide whether the previous web application
+ // needs to be restored rather than discarded. These are set only when the corresponding
+ // move returns, so a move that fails part way through (for example a cross-file-system
+ // copy that cannot delete its source) leaves the flag false.
+ boolean previousMovedAside = false;
+ boolean migratedPlaced = false;
try {
tempNew = File.createTempFile("new", null, host.getLegacyAppBaseFile());
tempOld = File.createTempFile("old", null, host.getLegacyAppBaseFile());
@@ -1216,8 +1222,10 @@ public class HostConfig implements LifecycleListener {
// Use rename
if (destination.exists()) {
Files.move(destination.toPath(), tempOld.toPath());
+ previousMovedAside = true;
}
Files.move(tempNew.toPath(), destination.toPath());
+ migratedPlaced = true;
// Only delete the previous webapp if everything went fine
ExpandWar.delete(tempOld);
@@ -1228,6 +1236,51 @@ public class HostConfig implements LifecycleListener {
if (tempNew != null && tempNew.exists()) {
ExpandWar.delete(tempNew);
}
+ if (tempOld != null) {
+ if (migratedPlaced) {
+ // The migrated application is in place. The previous application, if one was
+ // moved aside, is no longer needed.
+ ExpandWar.delete(tempOld);
+ } else if (previousMovedAside && tempOld.exists()) {
+ // The previous application was moved aside but the migrated application could
+ // not be placed. Restore the previous application so it is not lost. tempOld is
+ // known to hold the complete previous application because previousMovedAside is
+ // only set once the move out of the way returned successfully.
+ restorePreviousApplication(tempOld, destination);
+ }
+ // If the move out of the way never completed (previousMovedAside is false) tempOld
+ // is either a placeholder, an incomplete copy or, in the rare case of a
+ // cross-file-system move whose source deletion failed, possibly the only remaining
+ // copy of the previous application while the destination is incomplete. In that
+ // situation deleting tempOld could destroy data, so it is retained for manual
+ // recovery instead of being promoted over the destination.
+ }
+ }
+ }
+
+
+ /**
+ * Restore a previously migrated-out web application that is held in {@code tempOld} back to
+ * {@code destination}. Any incomplete content left at the destination by a failed move is
+ * removed first. If the restore cannot be completed, the copy in {@code tempOld} is retained so
+ * that an administrator can recover the original web application manually.
+ *
+ * @param tempOld the temporary location holding the complete previous web application
+ * @param destination the location the previous web application should be restored to
+ */
+ private void restorePreviousApplication(File tempOld, File destination) {
+ try {
+ if (destination.exists()) {
+ // Remove the partial content left by the failed move of the migrated application.
+ // The previous application is safely held in tempOld (previousMovedAside is set) so
+ // this does not risk the original data.
+ ExpandWar.delete(destination);
+ }
+ Files.move(tempOld.toPath(), destination.toPath());
+ log.warn(sm.getString("hostConfig.migrateRestore", destination));
+ } catch (Throwable t) {
+ ExceptionUtils.handleThrowable(t);
+ log.warn(sm.getString("hostConfig.migrateRestoreError", tempOld, destination), t);
}
}
diff --git a/java/org/apache/catalina/startup/LocalStrings.properties b/java/org/apache/catalina/startup/LocalStrings.properties
index 64493719d3..d19bc76a42 100644
--- a/java/org/apache/catalina/startup/LocalStrings.properties
+++ b/java/org/apache/catalina/startup/LocalStrings.properties
@@ -154,6 +154,8 @@ hostConfig.jmx.register=Register context [{0}] failed
hostConfig.jmx.unregister=Unregister context [{0}] failed
hostConfig.migrateApp.threaded.error=Error waiting for multi-thread migration of legacy applications to complete
hostConfig.migrateError=Migration failure
+hostConfig.migrateRestore=The previous web application was restored at [{0}] after the migrated application could not be placed
+hostConfig.migrateRestoreError=Unable to restore the previous web application from [{0}] to [{1}]. The copy at [{0}] has been retained for manual recovery.
hostConfig.reload=Reloading context [{0}]
hostConfig.resourceNotAbsolute=Unable to remove resource from context [{0}] since [{1}] is not an absolute path
hostConfig.start=HostConfig: Processing START
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index e2f354e113..a35e42a11d 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -1941,6 +1941,17 @@
<code>ServletContainerInitializer.onStartup()</code>, misconfiguring
the application at runtime. (remm)
</fix>
+ <fix>
+ When automatic migration of a legacy Java EE web application from
+ the <code>legacyAppBase</code> to the <code>appBase</code> failed
+ after the previous version of the application had been moved aside,
+ the previous application was left in a temporary location from which
+ it was neither restored nor cleaned up, making it effectively lost.
+ The previous application is now restored to the deployment location
+ when the migrated application cannot be placed, and is retained with
+ a warning in the temporary location only in the rare cases where
+ restoring it is not possible. (remm)
+ </fix>
</changelog>
</subsection>
<subsection name="Coyote">
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 23/23: Match Jakarta namespace JMS types when classifying @Resource annotations.
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 27c32481e2e27ffae82d82ab51270e69f729040f
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 17:37:55 2026 +0200
Match Jakarta namespace JMS types when classifying @Resource annotations.
The type-string dispatch in WebAnnotationSet.addResource() still compared the
pre-Jakarta javax.jms and javax.resource literals. Since the codebase uses the
jakarta.* namespace and neither the javax nor the jakarta JMS API is bundled
with Tomcat, the javax.jms.Queue and javax.jms.Topic comparisons could never
match, so a @Resource on a jakarta.jms.Queue or jakarta.jms.Topic member fell
through to the generic case and was registered as a resource-env-ref instead
of a message-destination-ref, losing the message-destination classification
and the usage metadata taken from the mapped name.
Use the jakarta.jms and jakarta.resource literals for the message-destination
and resource-ref branches. The resource-ref behaviour is unchanged either way
because the branch also matches any type ending in ConnectionFactory.
---
java/org/apache/catalina/startup/WebAnnotationSet.java | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/java/org/apache/catalina/startup/WebAnnotationSet.java b/java/org/apache/catalina/startup/WebAnnotationSet.java
index d3cfd2c316..707c5f2c12 100644
--- a/java/org/apache/catalina/startup/WebAnnotationSet.java
+++ b/java/org/apache/catalina/startup/WebAnnotationSet.java
@@ -295,11 +295,11 @@ public class WebAnnotationSet {
context.getNamingResources().addService(service);
- } else if (type.equals("javax.sql.DataSource") || type.equals("javax.jms.ConnectionFactory") ||
- type.equals("javax.jms.QueueConnectionFactory") || type.equals("javax.jms.TopicConnectionFactory") ||
- type.equals("jakarta.mail.Session") || type.equals("java.net.URL") ||
- type.equals("javax.resource.cci.ConnectionFactory") || type.equals("org.omg.CORBA_2_3.ORB") ||
- type.endsWith("ConnectionFactory")) {
+ } else if (type.equals("javax.sql.DataSource") || type.equals("jakarta.jms.ConnectionFactory") ||
+ type.equals("jakarta.jms.QueueConnectionFactory") ||
+ type.equals("jakarta.jms.TopicConnectionFactory") || type.equals("jakarta.mail.Session") ||
+ type.equals("java.net.URL") || type.equals("jakarta.resource.cci.ConnectionFactory") ||
+ type.equals("org.omg.CORBA_2_3.ORB") || type.endsWith("ConnectionFactory")) {
// resource-ref element
ContextResource resource = new ContextResource();
@@ -320,7 +320,7 @@ public class WebAnnotationSet {
context.getNamingResources().addResource(resource);
- } else if (type.equals("javax.jms.Queue") || type.equals("javax.jms.Topic")) {
+ } else if (type.equals("jakarta.jms.Queue") || type.equals("jakarta.jms.Topic")) {
// message-destination-ref
MessageDestinationRef resource = new MessageDestinationRef();
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 01/23: Fix StringIndexOutOfBoundsException for unterminated regular expression in SSI expressions
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit ea6775810b515eb5ca99e90e7dbf019b6c6352da
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 13:44:22 2026 +0200
Fix StringIndexOutOfBoundsException for unterminated regular expression in SSI expressions
When an SSI conditional expression contained a regular expression literal
without a closing slash, ExpressionTokenizer.nextToken() computed an end
offset one past the end of the expression, and building the token string
threw a StringIndexOutOfBoundsException. Since this is not a ParseException,
it bypassed the error handling of SSIConditional and SSIProcessor and
surfaced as a 500 error for the whole request.
Cap the token at the end of the expression when the closing slash is
missing, mirroring the pre-existing behavior for unterminated quoted
strings. The resulting token (e.g. "/x") is not recognized as a regular
expression by ExpressionParseTree and is compared as a plain string. Also
harden the end-of-input check against the index past the end that both
unterminated cases leave behind, and add a tokenizer test case for the
unterminated regular expression.
---
java/org/apache/catalina/ssi/ExpressionTokenizer.java | 6 ++++--
test/org/apache/catalina/ssi/TestExpressionTokenizer.java | 5 +++++
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/java/org/apache/catalina/ssi/ExpressionTokenizer.java b/java/org/apache/catalina/ssi/ExpressionTokenizer.java
index 36c0d519cd..825b82c30a 100644
--- a/java/org/apache/catalina/ssi/ExpressionTokenizer.java
+++ b/java/org/apache/catalina/ssi/ExpressionTokenizer.java
@@ -109,7 +109,7 @@ public class ExpressionTokenizer {
}
// Clear the current token val
tokenVal = null;
- if (index == length) {
+ if (index >= length) {
return TOKEN_END; // End of string
}
int start = index;
@@ -191,7 +191,9 @@ public class ExpressionTokenizer {
}
escaped = false;
}
- end = ++index;
+ // If the regular expression was not terminated, the token is the
+ // remainder of the expression, otherwise it includes the closing slash
+ end = Math.min(++index, length);
} else {
// End is the next whitespace character
for (; index < length; index++) {
diff --git a/test/org/apache/catalina/ssi/TestExpressionTokenizer.java b/test/org/apache/catalina/ssi/TestExpressionTokenizer.java
index eafa517b49..e4f78cdb55 100644
--- a/test/org/apache/catalina/ssi/TestExpressionTokenizer.java
+++ b/test/org/apache/catalina/ssi/TestExpressionTokenizer.java
@@ -101,6 +101,11 @@ public class TestExpressionTokenizer {
// Escaped slash inside a regular expression
parameterSets.add(new Object[] { "/a\\/b/",
new int[] { ExpressionTokenizer.TOKEN_STRING }, new String[] { "/a\\/b/" } });
+ // Unterminated regular expression: the token is the remainder
+ parameterSets.add(new Object[] { "a = /x",
+ new int[] { ExpressionTokenizer.TOKEN_STRING, ExpressionTokenizer.TOKEN_EQ,
+ ExpressionTokenizer.TOKEN_STRING },
+ new String[] { "a", null, "/x" } });
// Operators mixed with strings
parameterSets.add(new Object[] { "a && b",
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 18/23: Return an absolute home path from HomesUserDatabase when homeBase is relative.
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 186fca662f08898a2e209a640fe3faac3cffa397
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 16:48:53 2026 +0200
Return an absolute home path from HomesUserDatabase when homeBase is relative.
HomesUserDatabase.init() stored homeDir.toString(), which keeps the path
relative when the configured homeBase is relative. That violates the
UserDatabase.getHome() contract, which documents that it returns an absolute
pathname, and it produced an inconsistent resolution in UserConfig.deploy():
the user's web application directory existence check resolves a relative path
against the JVM working directory, while the same value passed to
Context.setDocBase() is later resolved against the host's appBase. The two
bases differ, so per-user applications could silently fail to deploy or, worse,
resolve to an unexpected existing directory.
Store homeDir.getAbsolutePath() so the value always honours the interface
contract and resolves to a single location. getAbsolutePath() is used rather
than getCanonicalPath() so that symlinked home directories keep working and no
IOException handling is introduced; there is no behavioural change when homeBase
is already absolute.
---
java/org/apache/catalina/startup/HomesUserDatabase.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/java/org/apache/catalina/startup/HomesUserDatabase.java b/java/org/apache/catalina/startup/HomesUserDatabase.java
index e1c0b3a286..5f269c9e37 100644
--- a/java/org/apache/catalina/startup/HomesUserDatabase.java
+++ b/java/org/apache/catalina/startup/HomesUserDatabase.java
@@ -90,7 +90,7 @@ public final class HomesUserDatabase implements UserDatabase {
if (!homeDir.isDirectory() || !homeDir.canRead()) {
continue;
}
- homes.put(homeBaseFile, homeDir.toString());
+ homes.put(homeBaseFile, homeDir.getAbsolutePath());
}
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 02/23: Report a parse error for SSI expressions with missing operands
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 1190183f588efaba1a6678fd9d58224d91251a57
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 13:53:48 2026 +0200
Report a parse error for SSI expressions with missing operands
Malformed SSI conditional expressions such as "a =", "= a" or "!"
made OppNode.popValues() call removeFirst() on an empty node stack,
throwing a NoSuchElementException out of the ExpressionParseTree
constructor. Since SSIConditional only handles ParseException, the
unchecked exception bypassed the SSI error handling and surfaced as a
500 error for the whole request.
Check before each pop that the node stack holds at least the number of
operands the operator needs (new OppNode.getOperandCount(), one for
NotNode, two otherwise) and throw a ParseException with a new
missingOperand message otherwise. Also let resolveGroup() tolerate an
empty operator stack, which occurs when the expression contains more
closing parentheses than opening ones and the artificial group marker
has been consumed; such expressions now parse leniently or produce a
ParseException, as already the case for balanced groups.
---
.../apache/catalina/ssi/ExpressionParseTree.java | 81 +++++++++++++++++-----
.../apache/catalina/ssi/LocalStrings.properties | 1 +
.../catalina/ssi/TestExpressionParseTree.java | 34 +++++++++
3 files changed, 98 insertions(+), 18 deletions(-)
diff --git a/java/org/apache/catalina/ssi/ExpressionParseTree.java b/java/org/apache/catalina/ssi/ExpressionParseTree.java
index 769196c770..4d24eded9d 100644
--- a/java/org/apache/catalina/ssi/ExpressionParseTree.java
+++ b/java/org/apache/catalina/ssi/ExpressionParseTree.java
@@ -86,9 +86,12 @@ public class ExpressionParseTree {
/**
* Pushes a new operator onto the opp stack, resolving existing opps as needed.
*
- * @param node The operator node
+ * @param node The operator node
+ * @param index The current tokenizer index, for error reporting
+ *
+ * @throws ParseException An operator was missing one of its operands
*/
- private void pushOpp(OppNode node) {
+ private void pushOpp(OppNode node, int index) throws ParseException {
// If node is null then it's just a group marker
if (node == null) {
oppStack.addFirst(null);
@@ -109,6 +112,7 @@ public class ExpressionParseTree {
if (top.getPrecedence() < node.getPrecedence()) {
break;
}
+ checkOperands(top, index);
// Remove the top node
oppStack.removeFirst();
// Let it fill its branches
@@ -121,12 +125,39 @@ public class ExpressionParseTree {
}
+ /**
+ * Checks that the node stack holds at least the number of operands the given
+ * operator needs.
+ *
+ * @param node The operator to check the operands for
+ * @param index The current tokenizer index, for error reporting
+ *
+ * @throws ParseException The operator is missing one of its operands
+ */
+ private void checkOperands(OppNode node, int index) throws ParseException {
+ if (nodeStack.size() < node.getOperandCount()) {
+ throw new ParseException(sm.getString("expressionParseTree.missingOperand"), index);
+ }
+ }
+
+
/**
* Resolves all pending opp nodes on the stack until the next group marker is reached.
+ *
+ * @param index The current tokenizer index, for error reporting
+ *
+ * @throws ParseException An operator was missing one of its operands
*/
- private void resolveGroup() {
- OppNode top;
- while ((top = oppStack.removeFirst()) != null) {
+ private void resolveGroup(int index) throws ParseException {
+ // The stack may be empty if the expression contained more closing
+ // parentheses than opening ones, which consumed the artificial group
+ // marker
+ while (!oppStack.isEmpty()) {
+ OppNode top = oppStack.removeFirst();
+ if (top == null) {
+ break;
+ }
+ checkOperands(top, index);
// Let it fill its branches
top.popValues(nodeStack);
// Stick it on the resolved node stack
@@ -146,7 +177,7 @@ public class ExpressionParseTree {
StringNode currStringNode = null;
// We cheat a little and start an artificial
// group right away. It makes finishing easier.
- pushOpp(null);
+ pushOpp(null, 0);
ExpressionTokenizer et = new ExpressionTokenizer(expr);
while (et.hasMoreTokens()) {
int token = et.nextToken();
@@ -165,55 +196,55 @@ public class ExpressionParseTree {
}
break;
case ExpressionTokenizer.TOKEN_AND:
- pushOpp(new AndNode());
+ pushOpp(new AndNode(), et.getIndex());
break;
case ExpressionTokenizer.TOKEN_OR:
- pushOpp(new OrNode());
+ pushOpp(new OrNode(), et.getIndex());
break;
case ExpressionTokenizer.TOKEN_NOT:
- pushOpp(new NotNode());
+ pushOpp(new NotNode(), et.getIndex());
break;
case ExpressionTokenizer.TOKEN_EQ:
- pushOpp(new EqualNode());
+ pushOpp(new EqualNode(), et.getIndex());
break;
case ExpressionTokenizer.TOKEN_NOT_EQ:
- pushOpp(new NotNode());
+ pushOpp(new NotNode(), et.getIndex());
// Sneak the regular node in. They will NOT
// be resolved when the next opp comes along.
oppStack.addFirst(new EqualNode());
break;
case ExpressionTokenizer.TOKEN_RBRACE:
// Closeout the current group
- resolveGroup();
+ resolveGroup(et.getIndex());
break;
case ExpressionTokenizer.TOKEN_LBRACE:
// Push a group marker
- pushOpp(null);
+ pushOpp(null, et.getIndex());
break;
case ExpressionTokenizer.TOKEN_GE:
- pushOpp(new NotNode());
+ pushOpp(new NotNode(), et.getIndex());
// Similar strategy to NOT_EQ above, except this
// is NOT less than
oppStack.addFirst(new LessThanNode());
break;
case ExpressionTokenizer.TOKEN_LE:
- pushOpp(new NotNode());
+ pushOpp(new NotNode(), et.getIndex());
// Similar strategy to NOT_EQ above, except this
// is NOT greater than
oppStack.addFirst(new GreaterThanNode());
break;
case ExpressionTokenizer.TOKEN_GT:
- pushOpp(new GreaterThanNode());
+ pushOpp(new GreaterThanNode(), et.getIndex());
break;
case ExpressionTokenizer.TOKEN_LT:
- pushOpp(new LessThanNode());
+ pushOpp(new LessThanNode(), et.getIndex());
break;
case ExpressionTokenizer.TOKEN_END:
break;
}
}
// Finish off the rest of the opps
- resolveGroup();
+ resolveGroup(et.getIndex());
if (nodeStack.isEmpty()) {
throw new ParseException(sm.getString("expressionParseTree.noNodes"), et.getIndex());
}
@@ -301,6 +332,14 @@ public class ExpressionParseTree {
public abstract int getPrecedence();
+ /**
+ * @return the number of operands this operator pops from the node stack, two by default.
+ */
+ public int getOperandCount() {
+ return 2;
+ }
+
+
/**
* Lets the node pop its own branch nodes off the front of the specified list. The default pulls two.
*
@@ -325,6 +364,12 @@ public class ExpressionParseTree {
}
+ @Override
+ public int getOperandCount() {
+ return 1;
+ }
+
+
/**
* Overridden to pop only one value.
*/
diff --git a/java/org/apache/catalina/ssi/LocalStrings.properties b/java/org/apache/catalina/ssi/LocalStrings.properties
index b5ea18ab25..5cebaec4d0 100644
--- a/java/org/apache/catalina/ssi/LocalStrings.properties
+++ b/java/org/apache/catalina/ssi/LocalStrings.properties
@@ -18,6 +18,7 @@
expressionParseTree.extraNodes=Extra nodes created
expressionParseTree.invalidExpression=Invalid expression [{0}]
+expressionParseTree.missingOperand=Missing operand
expressionParseTree.noNodes=No nodes created
expressionParseTree.unusedOpCodes=Unused nodes exist
diff --git a/test/org/apache/catalina/ssi/TestExpressionParseTree.java b/test/org/apache/catalina/ssi/TestExpressionParseTree.java
index 14b63233da..a635c8258d 100644
--- a/test/org/apache/catalina/ssi/TestExpressionParseTree.java
+++ b/test/org/apache/catalina/ssi/TestExpressionParseTree.java
@@ -17,6 +17,7 @@
package org.apache.catalina.ssi;
import java.io.IOException;
+import java.text.ParseException;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
@@ -136,6 +137,39 @@ public class TestExpressionParseTree {
}
+ @Test
+ public void testMissingOperand() throws Exception {
+ // Operators missing an operand must produce a parse error rather than
+ // an unchecked exception
+ String[] expressions = { "= a", "a =", "a ! =", "!", "a = = b", "!= a" };
+ for (String expression : expressions) {
+ SSIMediator mediator = new SSIMediator(new TesterSSIExternalResolver(), LAST_MODIFIED);
+ try {
+ new ExpressionParseTree(expression, mediator);
+ Assert.fail("Expected a parse error for [" + expression + "]");
+ } catch (ParseException pe) {
+ // Expected
+ }
+ }
+ }
+
+
+ @Test
+ public void testExtraClosingParen() throws Exception {
+ // Unbalanced closing parentheses consumed the artificial group marker
+ // and used to result in a NoSuchElementException from the parser
+ String[] expressions = { ")", "a)", "a))", "(a))", "a ) b" };
+ for (String expression : expressions) {
+ SSIMediator mediator = new SSIMediator(new TesterSSIExternalResolver(), LAST_MODIFIED);
+ try {
+ new ExpressionParseTree(expression, mediator);
+ } catch (ParseException pe) {
+ // Also a valid outcome
+ }
+ }
+ }
+
+
@Test
public void testSubstituteVariablesPlainVar() throws Exception {
SSIMediator mediator = new SSIMediator(new TesterSSIExternalResolver(), LAST_MODIFIED);
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 16/23: Use a consistent key when adding and removing serviced entries during legacy application migration.
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 579a90cca33a4dad75f4f696d3d33626e75c64f8
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 16:21:04 2026 +0200
Use a consistent key when adding and removing serviced entries during
legacy application migration.
migrateLegacyApps() guarded migration with tryAddServiced(cn.getBaseName())
but both removal paths (the submit failure catch block and the MigrateApp
runnable) call removeServiced(cn.getName()). Because ContextName.getBaseName()
(e.g. "app") and ContextName.getName() (e.g. "/app") are different strings,
the base-name entry was never removed from the serviced set. This left a stale
entry per migrated legacy application and, more importantly, caused every
subsequent periodic check to skip re-migration of that application (e.g. after
the admin replaces an updated WAR in legacyAppBase, or after a transient
migration failure) until Tomcat was restarted.
Key the add with cn.getName() so it matches the removals and the convention
used by every other serviced-set caller (deploy, undeploy, Manager, etc.).
---
java/org/apache/catalina/startup/HostConfig.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/java/org/apache/catalina/startup/HostConfig.java b/java/org/apache/catalina/startup/HostConfig.java
index 5e1dd88657..0e367fecc0 100644
--- a/java/org/apache/catalina/startup/HostConfig.java
+++ b/java/org/apache/catalina/startup/HostConfig.java
@@ -1164,7 +1164,7 @@ public class HostConfig implements LifecycleListener {
continue;
}
- if (tryAddServiced(cn.getBaseName())) {
+ if (tryAddServiced(cn.getName())) {
try {
// MigrateApp will call removeServiced
results.add(es.submit(new MigrateApp(this, cn, source, destination)));
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 11/23: Report the configured error message when a conditional SSI directive fails
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 0265a5d6e8f6cfb92587bca04b4a32f420ba6036
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 15:04:00 2026 +0200
Report the configured error message when a conditional SSI directive fails
The single catch site for SSIStopProcessingException in
SSIProcessor.process() assumes the command that threw has already
logged the failure and written the configured error message to the
response. That assumption did not hold for SSIConditional, which threw
bare when the expr attribute was missing or renamed and when the
expression could not be parsed, and for the unknown conditional
command branch. A malformed <!--#if--> therefore truncated the rest
of the document from the response with no error message and no log
entry, making the failure invisible and hard to diagnose.
SSIConditional now logs the failure and writes the configured error
message before throwing, mirroring the established error path of the
other commands such as SSISet, using the new ssiConditional.*
messages. In addition, SSIProcessor.process() now logs the exception
whenever it carries a wrapped cause, which previously was discarded
at the catch site by ExpressionParseTree.evaluateTree() wrapping
arbitrary Throwables, and whenever debugging is enabled, so no
failure path can remain silent.
Document truncation at a failing directive is unchanged; it mimics
Apache behaviour. Add TestSsiServlet.testBadConditionalExpressionReportsError
to cover the end-to-end behaviour.
---
.../apache/catalina/ssi/LocalStrings.properties | 4 +++
java/org/apache/catalina/ssi/SSIConditional.java | 16 ++++++++--
java/org/apache/catalina/ssi/SSIProcessor.java | 7 +++-
test/org/apache/catalina/ssi/TestSsiServlet.java | 37 ++++++++++++++++++++++
4 files changed, 60 insertions(+), 4 deletions(-)
diff --git a/java/org/apache/catalina/ssi/LocalStrings.properties b/java/org/apache/catalina/ssi/LocalStrings.properties
index 5cb718e6bd..5969272c58 100644
--- a/java/org/apache/catalina/ssi/LocalStrings.properties
+++ b/java/org/apache/catalina/ssi/LocalStrings.properties
@@ -25,6 +25,10 @@ expressionParseTree.unusedOpCodes=Unused nodes exist
ssiCommand.invalidAttribute=Invalid attribute [{0}]
+ssiConditional.errorEvaluatingExpression=Error evaluating expression [{0}]
+ssiConditional.noExpression=No expression specified
+ssiConditional.unknownCommand=Unknown conditional command [{0}]
+
ssiEcho.invalidEncoding=Invalid encoding [{0}]
ssiExec.executeFailed=Cannot execute file [{0}]
diff --git a/java/org/apache/catalina/ssi/SSIConditional.java b/java/org/apache/catalina/ssi/SSIConditional.java
index d122298265..014f196eaf 100644
--- a/java/org/apache/catalina/ssi/SSIConditional.java
+++ b/java/org/apache/catalina/ssi/SSIConditional.java
@@ -20,11 +20,15 @@ package org.apache.catalina.ssi;
import java.io.PrintWriter;
import java.text.ParseException;
+import org.apache.tomcat.util.res.StringManager;
+
/**
* SSI command that handles all conditional directives.
*/
public class SSIConditional implements SSICommand {
+ private static final StringManager sm = StringManager.getManager(SSIConditional.class);
+
/**
* Default constructor.
@@ -50,7 +54,7 @@ public class SSIConditional implements SSICommand {
}
state.nestingCount = 0;
// Evaluate the expression
- if (evaluateArguments(paramNames, paramValues, ssiMediator)) {
+ if (evaluateArguments(paramNames, paramValues, ssiMediator, writer)) {
// No more branches can be taken for this if block
state.branchTaken = true;
} else {
@@ -71,7 +75,7 @@ public class SSIConditional implements SSICommand {
return lastModified;
}
// Evaluate the expression
- if (evaluateArguments(paramNames, paramValues, ssiMediator)) {
+ if (evaluateArguments(paramNames, paramValues, ssiMediator, writer)) {
// Turn back on output and mark the branch
state.processConditionalCommandsOnly = false;
state.branchTaken = true;
@@ -106,6 +110,8 @@ public class SSIConditional implements SSICommand {
// in the first place.
state.branchTaken = true;
} else {
+ ssiMediator.log(sm.getString("ssiConditional.unknownCommand", commandName));
+ writer.write(ssiMediator.getConfigErrMsg());
throw new SSIStopProcessingException();
}
return lastModified;
@@ -115,16 +121,20 @@ public class SSIConditional implements SSICommand {
/**
* Retrieves the expression from the specified arguments and performs the necessary evaluation steps.
*/
- private boolean evaluateArguments(String[] names, String[] values, SSIMediator ssiMediator)
+ private boolean evaluateArguments(String[] names, String[] values, SSIMediator ssiMediator, PrintWriter writer)
throws SSIStopProcessingException {
String expr = getExpression(names, values);
if (expr == null) {
+ ssiMediator.log(sm.getString("ssiConditional.noExpression"));
+ writer.write(ssiMediator.getConfigErrMsg());
throw new SSIStopProcessingException();
}
try {
ExpressionParseTree tree = new ExpressionParseTree(expr, ssiMediator);
return tree.evaluateTree();
} catch (ParseException e) {
+ ssiMediator.log(sm.getString("ssiConditional.errorEvaluatingExpression", expr), e);
+ writer.write(ssiMediator.getConfigErrMsg());
throw new SSIStopProcessingException();
}
}
diff --git a/java/org/apache/catalina/ssi/SSIProcessor.java b/java/org/apache/catalina/ssi/SSIProcessor.java
index bfd3e0cce1..ca0a4aa41d 100644
--- a/java/org/apache/catalina/ssi/SSIProcessor.java
+++ b/java/org/apache/catalina/ssi/SSIProcessor.java
@@ -181,7 +181,12 @@ public class SSIProcessor {
}
}
} catch (SSIStopProcessingException e) {
- // If we are here, then we have already stopped processing, so all is good
+ // If we are here, then processing has already been stopped by a command that
+ // reported its own error. Log any wrapped cause so nothing can fail silently,
+ // and log everything when debugging is enabled.
+ if (debug > 0 || e.getCause() != null) {
+ ssiExternalResolver.log("SSI processing stopped", e);
+ }
}
return lastModifiedDate;
}
diff --git a/test/org/apache/catalina/ssi/TestSsiServlet.java b/test/org/apache/catalina/ssi/TestSsiServlet.java
index d1363782c3..3599530415 100644
--- a/test/org/apache/catalina/ssi/TestSsiServlet.java
+++ b/test/org/apache/catalina/ssi/TestSsiServlet.java
@@ -117,4 +117,41 @@ public class TestSsiServlet extends TomcatBaseTest {
}
Assert.assertNotNull(lastModified);
}
+
+
+ @Test
+ public void testBadConditionalExpressionReportsError() throws Exception {
+ // A conditional directive with an expression that cannot be parsed
+ // stops processing of the document, but it must report the default
+ // error message rather than truncating the response silently.
+ Tomcat tomcat = getTomcatInstance();
+
+ File appDir = new File(getTemporaryDirectory(), "ssi-badif");
+ Assert.assertTrue(appDir.mkdirs() || appDir.isDirectory());
+ addDeleteOnTearDown(appDir);
+ File doc = new File(appDir, "badif.shtml");
+ try (Writer writer = new OutputStreamWriter(new FileOutputStream(doc), StandardCharsets.ISO_8859_1)) {
+ writer.write("BEFORE-CONTENT\n");
+ writer.write("<!--#if expr=\"this is not valid (\"-->\n");
+ writer.write("AFTER-CONTENT\n");
+ }
+
+ Context ctxt = tomcat.addContext("", appDir.getAbsolutePath());
+ Tomcat.addServlet(ctxt, "ssi", new SSIServlet());
+ ctxt.addServletMapping("*.shtml", "ssi");
+
+ tomcat.start();
+
+ Map<String,List<String>> resHeaders = new HashMap<>();
+ String path = "http://localhost:" + getPort() + "/badif.shtml";
+ ByteChunk out = new ByteChunk();
+
+ int rc = getUrl(path, out, resHeaders);
+ Assert.assertEquals(HttpServletResponse.SC_OK, rc);
+ String body = new String(out.getBuffer(), 0, out.getLength(), StandardCharsets.ISO_8859_1);
+ Assert.assertTrue(body.contains("BEFORE-CONTENT"));
+ Assert.assertTrue(body.contains("[an error occurred"));
+ // Processing of the remainder of the document stops at the failing directive
+ Assert.assertFalse(body.contains("AFTER-CONTENT"));
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 09/23: Keep the Last-Modified header for large unbuffered SSI documents
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 57b551577f556aecf2ec488b6c8d1a7fbf93c4f7
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 14:46:53 2026 +0200
Keep the Last-Modified header for large unbuffered SSI documents
In the default unbuffered mode, SSIServlet streams the processed output
directly to the response writer. Documents larger than the response
buffer commit the response while processing, so the Last-Modified header
that was set after processing completed was silently ignored and large
SSI pages were served without it.
Set the header from the source resource's last modified time before the
output is generated, and upgrade it to the value aggregated from the
directives afterwards only when it is newer and the response has not
been committed yet. This keeps the previous behavior for buffered
processing and responses that stay uncommitted.
---
java/org/apache/catalina/ssi/SSIServlet.java | 11 ++++--
test/org/apache/catalina/ssi/TestSsiServlet.java | 45 ++++++++++++++++++++++++
2 files changed, 54 insertions(+), 2 deletions(-)
diff --git a/java/org/apache/catalina/ssi/SSIServlet.java b/java/org/apache/catalina/ssi/SSIServlet.java
index 2a2fe53dea..8281689b87 100644
--- a/java/org/apache/catalina/ssi/SSIServlet.java
+++ b/java/org/apache/catalina/ssi/SSIServlet.java
@@ -211,8 +211,15 @@ public class SSIServlet extends HttpServlet {
}
try (BufferedReader bufferedReader = new BufferedReader(isr)) {
- long lastModified = ssiProcessor.process(bufferedReader, resourceInfo.getLastModified(), printWriter);
- if (lastModified > 0) {
+ // Set the header from the source before the output is generated. In the
+ // unbuffered mode a response larger than the response buffer is committed
+ // while processing, after which setting the header is silently ignored.
+ long sourceLastModified = resourceInfo.getLastModified();
+ if (sourceLastModified > 0) {
+ res.setDateHeader("last-modified", sourceLastModified);
+ }
+ long lastModified = ssiProcessor.process(bufferedReader, sourceLastModified, printWriter);
+ if (lastModified > sourceLastModified && !res.isCommitted()) {
res.setDateHeader("last-modified", lastModified);
}
if (stringWriter != null) {
diff --git a/test/org/apache/catalina/ssi/TestSsiServlet.java b/test/org/apache/catalina/ssi/TestSsiServlet.java
index 28e8fa6337..d1363782c3 100644
--- a/test/org/apache/catalina/ssi/TestSsiServlet.java
+++ b/test/org/apache/catalina/ssi/TestSsiServlet.java
@@ -17,6 +17,9 @@
package org.apache.catalina.ssi;
import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
@@ -72,4 +75,46 @@ public class TestSsiServlet extends TomcatBaseTest {
Assert.assertFalse(body.contains("(none)"));
}
+
+
+ @Test
+ public void testLargeDocumentLastModifiedHeader() throws Exception {
+ // A document larger than the response buffer commits the unbuffered
+ // response while the output is being generated. The Last-Modified
+ // header must be set before that happens.
+ Tomcat tomcat = getTomcatInstance();
+
+ File appDir = new File(getTemporaryDirectory(), "ssi-large");
+ Assert.assertTrue(appDir.mkdirs() || appDir.isDirectory());
+ addDeleteOnTearDown(appDir);
+ File doc = new File(appDir, "large.shtml");
+ try (Writer writer = new OutputStreamWriter(new FileOutputStream(doc), StandardCharsets.ISO_8859_1)) {
+ writer.write("X".repeat(20000));
+ writer.write("<!--#flastmod file=\"large.shtml\" -->TAIL");
+ }
+
+ Context ctxt = tomcat.addContext("", appDir.getAbsolutePath());
+ Tomcat.addServlet(ctxt, "ssi", new SSIServlet());
+ ctxt.addServletMapping("*.shtml", "ssi");
+
+ tomcat.start();
+
+ Map<String,List<String>> resHeaders = new HashMap<>();
+ String path = "http://localhost:" + getPort() + "/large.shtml";
+ ByteChunk out = new ByteChunk();
+
+ int rc = getUrl(path, out, resHeaders);
+ Assert.assertEquals(HttpServletResponse.SC_OK, rc);
+ String body = new String(out.getBuffer(), 0, out.getLength(), StandardCharsets.ISO_8859_1);
+ Assert.assertTrue(body.endsWith("TAIL"));
+ // Headers are case-insensitive. SSIServlet uses the lower-case name.
+ String lastModified = null;
+ for (Map.Entry<String,List<String>> header : resHeaders.entrySet()) {
+ if ("Last-Modified".equalsIgnoreCase(header.getKey())) {
+ Assert.assertEquals(1, header.getValue().size());
+ lastModified = header.getValue().get(0);
+ }
+ }
+ Assert.assertNotNull(lastModified);
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 03/23: Give && higher precedence than || in SSI conditional expressions
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 0ff7f766aeddfde47a6cc23b07de385e555d090b
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 13:59:26 2026 +0200
Give && higher precedence than || in SSI conditional expressions
The SSI conditional expression parser assigned the same precedence to
the logical operators, so mixed chains were evaluated strictly left to
right and "$a || $b && $c" grouped as "($a || $b) && $c" instead of
the conventionally expected "$a || ($b && $c)", silently inverting the
logic of such expressions.
Give AndNode and OrNode distinct precedences, with && binding more
tightly than ||, consistent with the convention used by the C-like
languages and with Apache HTTP Server expressions. Chains of the same
operator keep their left-to-right associativity. Note that this changes
the evaluation of existing documents using unparenthesized mixed && and
|| chains. Document the precedence order in the SSI howto.
---
java/org/apache/catalina/ssi/ExpressionParseTree.java | 7 ++++---
.../apache/catalina/ssi/TestExpressionParseTree.java | 19 +++++++++++++++++++
webapps/docs/changelog.xml | 12 ++++++++++++
webapps/docs/ssi-howto.xml | 6 ++++++
4 files changed, 41 insertions(+), 3 deletions(-)
diff --git a/java/org/apache/catalina/ssi/ExpressionParseTree.java b/java/org/apache/catalina/ssi/ExpressionParseTree.java
index 4d24eded9d..d18cb7b948 100644
--- a/java/org/apache/catalina/ssi/ExpressionParseTree.java
+++ b/java/org/apache/catalina/ssi/ExpressionParseTree.java
@@ -310,7 +310,8 @@ public class ExpressionParseTree {
private static final int PRECEDENCE_NOT = 5;
private static final int PRECEDENCE_COMPARE = 4;
- private static final int PRECEDENCE_LOGICAL = 1;
+ private static final int PRECEDENCE_AND = 2;
+ private static final int PRECEDENCE_OR = 1;
/**
* A node implementation that represents an operation.
@@ -397,7 +398,7 @@ public class ExpressionParseTree {
@Override
public int getPrecedence() {
- return PRECEDENCE_LOGICAL;
+ return PRECEDENCE_AND;
}
@@ -419,7 +420,7 @@ public class ExpressionParseTree {
@Override
public int getPrecedence() {
- return PRECEDENCE_LOGICAL;
+ return PRECEDENCE_OR;
}
diff --git a/test/org/apache/catalina/ssi/TestExpressionParseTree.java b/test/org/apache/catalina/ssi/TestExpressionParseTree.java
index a635c8258d..4865305a3c 100644
--- a/test/org/apache/catalina/ssi/TestExpressionParseTree.java
+++ b/test/org/apache/catalina/ssi/TestExpressionParseTree.java
@@ -137,6 +137,25 @@ public class TestExpressionParseTree {
}
+ @Test
+ public void testAndHasHigherPrecedenceThanOr() throws Exception {
+ SSIExternalResolver r = new TesterSSIExternalResolver();
+ r.setVariableValue("a", "x");
+ r.setVariableValue("b", "");
+ r.setVariableValue("c", "");
+ SSIMediator mediator = new SSIMediator(r, LAST_MODIFIED);
+ // With conventional precedence, this is true: $a || ($b && $c)
+ ExpressionParseTree ept = new ExpressionParseTree("$a || $b && $c", mediator);
+ Assert.assertTrue(ept.evaluateTree());
+ // Explicit parentheses give the same result
+ ept = new ExpressionParseTree("$a || ($b && $c)", mediator);
+ Assert.assertTrue(ept.evaluateTree());
+ // The left-to-right grouping evaluates to false: ($a || $b) && $c
+ ept = new ExpressionParseTree("($a || $b) && $c", mediator);
+ Assert.assertFalse(ept.evaluateTree());
+ }
+
+
@Test
public void testMissingOperand() throws Exception {
// Operators missing an operand must produce a parse error rather than
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 49e6a44fdf..4975384ef6 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -1903,6 +1903,18 @@
now passed to the <code>LoginModule</code> unchanged, consistent with
the non-JAAS realms. (remm)
</fix>
+ <fix>
+ Give the SSI conditional expression operators <code>&&</code>
+ and <code>||</code> distinct precedences, <code>&&</code>
+ binding more tightly than <code>||</code>, consistent with the
+ convention used by the C-like languages and by Apache HTTP Server
+ expressions. Previously, mixed chains were evaluated strictly left to
+ right, so <code>$a || $b && $c</code> was evaluated as
+ <code>($a || $b) && $c</code> rather than the expected
+ <code>$a || ($b && $c)</code>. Note that this changes the
+ evaluation of existing documents using unparenthesized mixed
+ <code>&&</code> and <code>||</code> chains. (remm)
+ </fix>
</changelog>
</subsection>
<subsection name="Coyote">
diff --git a/webapps/docs/ssi-howto.xml b/webapps/docs/ssi-howto.xml
index 7790307726..e121af40f4 100644
--- a/webapps/docs/ssi-howto.xml
+++ b/webapps/docs/ssi-howto.xml
@@ -219,6 +219,12 @@ is used to assign a value to a user-defined variable.
<!--#else -->
<p>Yoga class at noon.</p>
<!--#endif -->]]></source>
+Operators in conditional expressions are evaluated from highest to lowest
+precedence: parentheses, the <code>!</code> operator, the comparison operators
+(<code>=</code>, <code>!=</code>, <code><</code>, <code>></code>,
+<code><=</code>, <code>>=</code>), <code>&&</code> and finally
+<code>||</code>. For example, <code>$a || $b && $c</code> is evaluated as
+<code>$a || ($b && $c)</code>.
</li>
</ul>
<p>
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 08/23: Accept the var and value attributes of SSI #set in any order
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 925493100c1271d4347d1d533837d985bdbaa5ee
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 14:28:57 2026 +0200
Accept the var and value attributes of SSI #set in any order
SSISet paired a value attribute with a var attribute seen earlier in the
directive, so "<!--#set value=\"bar\" var=\"foo\" -->" which is valid for
Apache hit the missing variable error path, wrote the error message and
stopped processing, silently dropping the rest of the document.
Collect the attributes in a first pass so they can be given in any order
as Apache does, with later occurrences of the same attribute winning,
and reject unknown attributes during that pass. The other attribute
combinations keep their existing behavior.
---
java/org/apache/catalina/ssi/SSISet.java | 24 ++--
test/org/apache/catalina/ssi/TestSSISet.java | 158 +++++++++++++++++++++++++++
2 files changed, 173 insertions(+), 9 deletions(-)
diff --git a/java/org/apache/catalina/ssi/SSISet.java b/java/org/apache/catalina/ssi/SSISet.java
index f74b4cddf8..5b364d82c0 100644
--- a/java/org/apache/catalina/ssi/SSISet.java
+++ b/java/org/apache/catalina/ssi/SSISet.java
@@ -53,27 +53,33 @@ public class SSISet implements SSICommand {
long lastModified = 0;
String errorMessage = ssiMediator.getConfigErrMsg();
String variableName = null;
+ String variableValue = null;
+ // Collect the attributes first so that they can be given in any order, as
+ // Apache does
for (int i = 0; i < paramNames.length; i++) {
String paramName = paramNames[i];
String paramValue = paramValues[i];
if (paramName.equalsIgnoreCase("var")) {
variableName = paramValue;
} else if (paramName.equalsIgnoreCase("value")) {
- if (variableName != null) {
- String substitutedValue = ssiMediator.substituteVariables(paramValue);
- ssiMediator.setVariableValue(variableName, substitutedValue);
- lastModified = System.currentTimeMillis();
- } else {
- ssiMediator.log(sm.getString("ssiSet.noVariable"));
- writer.write(errorMessage);
- throw new SSIStopProcessingException();
- }
+ variableValue = paramValue;
} else {
ssiMediator.log(sm.getString("ssiCommand.invalidAttribute", paramName));
writer.write(errorMessage);
throw new SSIStopProcessingException();
}
}
+ if (variableName == null) {
+ if (variableValue != null) {
+ ssiMediator.log(sm.getString("ssiSet.noVariable"));
+ writer.write(errorMessage);
+ throw new SSIStopProcessingException();
+ }
+ } else if (variableValue != null) {
+ String substitutedValue = ssiMediator.substituteVariables(variableValue);
+ ssiMediator.setVariableValue(variableName, substitutedValue);
+ lastModified = System.currentTimeMillis();
+ }
return lastModified;
}
}
\ No newline at end of file
diff --git a/test/org/apache/catalina/ssi/TestSSISet.java b/test/org/apache/catalina/ssi/TestSSISet.java
new file mode 100644
index 0000000000..77e351bf3f
--- /dev/null
+++ b/test/org/apache/catalina/ssi/TestSSISet.java
@@ -0,0 +1,158 @@
+/*
+ * 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.catalina.ssi;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestSSISet {
+
+ @Test
+ public void testValueBeforeVar() throws Exception {
+ // The attributes may be given in any order, as Apache does
+ SSIMediator mediator = newMediator();
+ String output = process(mediator, new String[] { "value", "var" }, new String[] { "bar", "foo" });
+ Assert.assertEquals("", output);
+ Assert.assertEquals("bar", mediator.getVariableValue("foo"));
+ }
+
+
+ @Test
+ public void testVarBeforeValue() throws Exception {
+ SSIMediator mediator = newMediator();
+ String output = process(mediator, new String[] { "var", "value" }, new String[] { "foo", "bar" });
+ Assert.assertEquals("", output);
+ Assert.assertEquals("bar", mediator.getVariableValue("foo"));
+ }
+
+
+ @Test
+ public void testVarOnly() throws Exception {
+ // Setting only the variable name does nothing and is not an error
+ SSIMediator mediator = newMediator();
+ String output = process(mediator, new String[] { "var" }, new String[] { "foo" });
+ Assert.assertEquals("", output);
+ Assert.assertNull(mediator.getVariableValue("foo"));
+ }
+
+
+ @Test
+ public void testValueWithoutVar() throws Exception {
+ SSIMediator mediator = newMediator();
+ String output = process(mediator, new String[] { "value" }, new String[] { "bar" });
+ Assert.assertEquals(mediator.getConfigErrMsg(), output);
+ Assert.assertNull(mediator.getVariableValue("foo"));
+ }
+
+
+ @Test
+ public void testDuplicateAttributes() throws Exception {
+ // Later occurrences of an attribute win, regardless of order
+ SSIMediator mediator = newMediator();
+ String output = process(mediator, new String[] { "value", "value", "var" },
+ new String[] { "bar1", "bar2", "foo" });
+ Assert.assertEquals("", output);
+ Assert.assertEquals("bar2", mediator.getVariableValue("foo"));
+
+ mediator = newMediator();
+ output = process(mediator, new String[] { "var", "value", "value" },
+ new String[] { "foo", "bar1", "bar2" });
+ Assert.assertEquals("", output);
+ Assert.assertEquals("bar2", mediator.getVariableValue("foo"));
+ }
+
+
+ @Test
+ public void testInvalidAttribute() throws Exception {
+ SSIMediator mediator = newMediator();
+ String output = process(mediator, new String[] { "name" }, new String[] { "foo" });
+ Assert.assertEquals(mediator.getConfigErrMsg(), output);
+ }
+
+
+ private SSIMediator newMediator() {
+ return new SSIMediator(new TesterSSIExternalResolver(), 0);
+ }
+
+
+ private String process(SSIMediator mediator, String[] paramNames, String[] paramValues) throws Exception {
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter writer = new PrintWriter(stringWriter);
+ try {
+ new SSISet().process(mediator, "set", paramNames, paramValues, writer);
+ } catch (SSIStopProcessingException e) {
+ // Expected for the error cases
+ }
+ writer.flush();
+ return stringWriter.toString();
+ }
+
+ /**
+ * Minimal implementation that provides the bare essentials required for the unit tests.
+ */
+ private static class TesterSSIExternalResolver implements SSIExternalResolver {
+
+ private final Map<String,String> variables = new HashMap<>();
+
+ @Override
+ public void addVariableNames(Collection<String> variableNames) {
+ // NO-OP
+ }
+
+ @Override
+ public String getVariableValue(String name) {
+ return variables.get(name);
+ }
+
+ @Override
+ public void setVariableValue(String name, String value) {
+ variables.put(name, value);
+ }
+
+ @Override
+ public Date getCurrentDate() {
+ return null;
+ }
+
+ @Override
+ public long getFileSize(String path, boolean virtual) {
+ return 0;
+ }
+
+ @Override
+ public long getFileLastModified(String path, boolean virtual) {
+ return 0;
+ }
+
+ @Override
+ public String getFileText(String path, boolean virtual) {
+ return null;
+ }
+
+ @Override
+ public void log(String message, Throwable throwable) {
+ // NO-OP
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 12/23: Fix data races in parallel annotation scanning
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit bd06bc435c14a1cf63c9f30922b1051caa7519fa
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 15:13:06 2026 +0200
Fix data races in parallel annotation scanning
When the parallelAnnotationScanning attribute of StandardContext is
enabled, ContextConfig.processAnnotations() dispatches one scan task
per web fragment to the server utility executor. The tasks share the
per-ServletContainerInitializer class sets of initializerClassMap and
the shared java class cache, and none of the shared updates were
synchronised.
The per-SCI sets were plain HashSets mutated concurrently from the
scan tasks, so matched classes could be silently lost or the
underlying map corrupted. All SCIs are registered before scanning
starts, so the sets are now created as concurrent sets and the map
itself is only read during scanning; the concurrent computeIfAbsent()
that could structurally mutate the LinkedHashMap is gone.
Investigation showed the race was not confined to the value sets.
populateJavaClassCache() inserts a class before its super class and
interfaces, so a second thread that saw the partially populated cache
could compute an incomplete set of interested initializers for a
class and cache it permanently. populateSCIsForCacheEntry() now
loads the super class or interface entry on demand instead of
treating a cache miss as proof of absence, so the computed set is
always complete. The lazily computed per-entry set is also now
volatile for safe publication between the scanner threads.
Verified with a deployment harness using eight fragment JARs and an
SCI with @HandlesTypes(Object.class): deployments lost classes from
onStartup() before the fix and produced the exact expected set in
every run after it.
---
.../org/apache/catalina/startup/ContextConfig.java | 32 ++++++++++++++++++----
webapps/docs/changelog.xml | 14 ++++++++++
2 files changed, 41 insertions(+), 5 deletions(-)
diff --git a/java/org/apache/catalina/startup/ContextConfig.java b/java/org/apache/catalina/startup/ContextConfig.java
index dd5d2b3051..1d18e72359 100644
--- a/java/org/apache/catalina/startup/ContextConfig.java
+++ b/java/org/apache/catalina/startup/ContextConfig.java
@@ -1845,7 +1845,9 @@ public class ContextConfig implements LifecycleListener {
}
for (ServletContainerInitializer sci : detectedScis) {
- initializerClassMap.put(sci, new HashSet<>());
+ // The per-SCI sets are updated concurrently when parallel annotation
+ // scanning is enabled
+ initializerClassMap.put(sci, ConcurrentHashMap.newKeySet());
HandlesTypes ht;
try {
@@ -2440,9 +2442,12 @@ public class ContextConfig implements LifecycleListener {
return;
}
+ // All SCIs are registered in processServletContainerInitializers()
+ // before scanning starts, so the map is only read here. The value
+ // sets are updated concurrently when parallel annotation scanning
+ // is enabled.
for (ServletContainerInitializer sci : entry.getSciSet()) {
- Set<Class<?>> classes = initializerClassMap.computeIfAbsent(sci, k -> new HashSet<>());
- classes.add(clazz);
+ initializerClassMap.get(sci).add(clazz);
}
}
}
@@ -2545,7 +2550,15 @@ public class ContextConfig implements LifecycleListener {
return;
}
- // May be null of the class is not present or could not be loaded.
+ // May be null if the class is not present or could not be loaded.
+ if (superClassCacheEntry == null) {
+ // With parallel annotation scanning the entry may also be absent because
+ // another thread has not finished adding the hierarchy to the cache.
+ // Ensure the entry is present, loading the class if necessary, so the
+ // set computed here cannot miss an SCI that matches a super class.
+ populateJavaClassCache(superClassName, javaClassCache);
+ superClassCacheEntry = javaClassCache.get(superClassName);
+ }
if (superClassCacheEntry != null) {
if (superClassCacheEntry.getSciSet() == null) {
populateSCIsForCacheEntry(superClassCacheEntry, javaClassCache);
@@ -2557,6 +2570,13 @@ public class ContextConfig implements LifecycleListener {
// Interfaces
for (String interfaceName : cacheEntry.getInterfaceNames()) {
JavaClassCacheEntry interfaceEntry = javaClassCache.get(interfaceName);
+ // As for the super class, a null may mean the parallel scanner has
+ // not finished populating the cache, so attempt the load before
+ // deciding there is nothing of interest.
+ if (interfaceEntry == null) {
+ populateJavaClassCache(interfaceName, javaClassCache);
+ interfaceEntry = javaClassCache.get(interfaceName);
+ }
// A null could mean that the class not present in application or
// that there is nothing of interest. Either way, nothing to do here
// so move along
@@ -2965,8 +2985,10 @@ public class ContextConfig implements LifecycleListener {
/**
* The set of ServletContainerInitializers interested in this class, or {@link #EMPTY_SCI_SET} if none.
+ * Volatile for safe publication: the set is computed once and the reference may be handed between
+ * threads when annotation scanning runs in parallel.
*/
- private Set<ServletContainerInitializer> sciSet = null;
+ private volatile Set<ServletContainerInitializer> sciSet = null;
/**
* Constructs a new cache entry from a parsed Java class.
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index c98f19f0cd..e2f354e113 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -1927,6 +1927,20 @@
they are per-document values that do not need to be stored in the
external resolver. (remm)
</fix>
+ <fix>
+ Fix multiple data races in annotation scanning when the
+ <code>parallelAnnotationScanning</code> attribute of the
+ <code>StandardContext</code> is enabled. Classes matched for a
+ <code>ServletContainerInitializer</code> were collected into
+ non-thread-safe sets, which could silently lose matches. A scanner
+ thread could also cache an incomplete set of initializers for a class
+ when another thread had not finished adding that class' super class or
+ interfaces to the shared class cache, and the lazily computed set was
+ not safely published between threads. Matched classes could therefore
+ be missing from the set passed to
+ <code>ServletContainerInitializer.onStartup()</code>, misconfiguring
+ the application at runtime. (remm)
+ </fix>
</changelog>
</subsection>
<subsection name="Coyote">
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 14/23: Send the server shutdown command as UTF-8 and time out the stop connect
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 4dfe4ca9924cdbb195070c11bb288a5a1cf755cc
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 15:42:33 2026 +0200
Send the server shutdown command as UTF-8 and time out the stop connect
Catalina.stopServer() wrote the configured shutdown string to the
shutdown port one character at a time with OutputStream.write(int),
which only emits the low eight bits of each char. Any shutdown string
containing a character above U+00FF was corrupted in transit, so the
comparison in StandardServer.await() could never match and
catalina stop could never succeed. The stop command also opened the
socket with the no-timeout constructor, so against a shutdown port
that silently drops packets it blocked for the full operating system
TCP connect timeout.
The command is now written as UTF-8 bytes and the server side reads
the raw bytes and decodes them as UTF-8 before matching, so any
shutdown string works and the two sides agree on the encoding. ASCII
commands, the overwhelmingly common case, are unaffected. The client
now connects with a ten second timeout and reports the failure
instead of blocking indefinitely.
---
java/org/apache/catalina/core/StandardServer.java | 14 +++++++++----
java/org/apache/catalina/startup/Catalina.java | 25 +++++++++++++++++------
2 files changed, 29 insertions(+), 10 deletions(-)
diff --git a/java/org/apache/catalina/core/StandardServer.java b/java/org/apache/catalina/core/StandardServer.java
index 3dca67b94d..428e2de1ea 100644
--- a/java/org/apache/catalina/core/StandardServer.java
+++ b/java/org/apache/catalina/core/StandardServer.java
@@ -18,6 +18,7 @@ package org.apache.catalina.core;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -25,6 +26,7 @@ import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
+import java.nio.charset.StandardCharsets;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
@@ -551,7 +553,7 @@ public final class StandardServer extends LifecycleMBeanBase implements Server {
// Wait for the next connection
Socket socket = null;
- StringBuilder command = new StringBuilder();
+ ByteArrayOutputStream commandBytes = new ByteArrayOutputStream();
try {
InputStream stream;
long acceptStartTime = System.nanoTime();
@@ -594,7 +596,7 @@ public final class StandardServer extends LifecycleMBeanBase implements Server {
if (ch < 32 || ch == 127) {
break;
}
- command.append((char) ch);
+ commandBytes.write(ch);
expected--;
}
} finally {
@@ -608,13 +610,17 @@ public final class StandardServer extends LifecycleMBeanBase implements Server {
}
}
+ // Decode the received bytes as UTF-8, matching the encoding
+ // used by the stop command
+ String command = commandBytes.toString(StandardCharsets.UTF_8);
+
// Match against our command string
- boolean match = command.toString().equals(shutdown);
+ boolean match = command.equals(shutdown);
if (match) {
log.info(sm.getString("standardServer.shutdownViaPort"));
break;
} else {
- log.warn(sm.getString("standardServer.invalidShutdownCommand", command.toString()));
+ log.warn(sm.getString("standardServer.invalidShutdownCommand", command));
}
}
} finally {
diff --git a/java/org/apache/catalina/startup/Catalina.java b/java/org/apache/catalina/startup/Catalina.java
index 1feded95d6..5d514d9d0e 100644
--- a/java/org/apache/catalina/startup/Catalina.java
+++ b/java/org/apache/catalina/startup/Catalina.java
@@ -24,7 +24,9 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.net.ConnectException;
+import java.net.InetSocketAddress;
import java.net.Socket;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -79,6 +81,12 @@ public class Catalina {
*/
public static final String SERVER_XML = "conf/server.xml";
+ /**
+ * Timeout, in milliseconds, for connecting to the shutdown port when
+ * stopping the server.
+ */
+ private static final int SHUTDOWN_CONNECT_TIMEOUT_MS = 10000;
+
// ----------------------------------------------------- Instance Variables
/**
@@ -772,12 +780,17 @@ public class Catalina {
// Stop the existing server
s = getServer();
if (s.getPortWithOffset() > 0) {
- try (Socket socket = new Socket(s.getAddress(), s.getPortWithOffset());
- OutputStream stream = socket.getOutputStream()) {
- String shutdown = s.getShutdown();
- for (int i = 0; i < shutdown.length(); i++) {
- stream.write(shutdown.charAt(i));
- }
+ // Use a connect timeout so a shutdown port that silently drops
+ // packets does not block the stop command indefinitely
+ try (Socket socket = new Socket()) {
+ socket.connect(new InetSocketAddress(s.getAddress(), s.getPortWithOffset()),
+ SHUTDOWN_CONNECT_TIMEOUT_MS);
+ OutputStream stream = socket.getOutputStream();
+ // Send the shutdown command as UTF-8 bytes. Writing the
+ // characters one at a time with OutputStream.write(int) would
+ // silently truncate any character above U+00FF and the server
+ // would then never match the configured shutdown string.
+ stream.write(s.getShutdown().getBytes(StandardCharsets.UTF_8));
stream.flush();
} catch (ConnectException ce) {
log.error(sm.getString("catalina.stopServer.connectException", s.getAddress(),
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org
(tomcat) 21/23: Normalise a context path missing its leading slash in ContextName.
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 3092acd16c64953388590d4c3a57fce56aca0f7b
Author: opencode <re...@apache.org>
AuthorDate: Thu Oct 8 17:09:48 2026 +0200
Normalise a context path missing its leading slash in ContextName.
The ContextName(String path, String version) constructor documented that a
non-empty path must start with "/" but did not check it. The base name is
derived with path.substring(1), so a path such as "foo" silently lost its
first character and produced the base name "oo". This reached Tomcat's
addWebapp(String, URL): the WAR was copied to appBase/oo.war while the
context was later deployed as "/foo" (Context.setPath() prepends the missing
slash), and the file name no longer corresponded to the deployed context.
Prepend the missing leading slash for paths that lack it, mirroring the
normalisation Context.setPath() performs. Well-formed inputs are unaffected,
so valid callers (which already pass normalized context paths) produce
identical results. A test covering the malformed input is added.
---
java/org/apache/catalina/util/ContextName.java | 10 ++++++++--
test/org/apache/catalina/util/TestContextName.java | 13 +++++++++++++
2 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/java/org/apache/catalina/util/ContextName.java b/java/org/apache/catalina/util/ContextName.java
index 01cf3805c7..cd871b9a43 100644
--- a/java/org/apache/catalina/util/ContextName.java
+++ b/java/org/apache/catalina/util/ContextName.java
@@ -102,15 +102,21 @@ public final class ContextName {
*
* @param path Context path to use, which must not contain a version marker. May be <code>null</code>,
* <code>""</code>, <code>"/"</code> or <code>"/ROOT"</code> to represent the root context. Any
- * other value must start with <code>"/"</code>
+ * other value must start with <code>"/"</code>; if it does not, a leading <code>"/"</code> is
+ * prepended, matching the normalisation performed by
+ * {@link org.apache.catalina.Context#setPath(String)}.
* @param version Context version to use
*/
public ContextName(String path, String version) {
// Path should never be null, '/' or '/ROOT'
if (path == null || "/".equals(path) || "/ROOT".equals(path)) {
this.path = "";
- } else {
+ } else if (path.startsWith("/") || path.isEmpty()) {
this.path = path;
+ } else {
+ // Normalise a malformed path the same way Context.setPath() does by adding the missing
+ // leading slash, so that the conversions below derive the expected name and base name
+ this.path = '/' + path;
}
// Version should never be null
diff --git a/test/org/apache/catalina/util/TestContextName.java b/test/org/apache/catalina/util/TestContextName.java
index ff8d62e81c..fa6d85cf6f 100644
--- a/test/org/apache/catalina/util/TestContextName.java
+++ b/test/org/apache/catalina/util/TestContextName.java
@@ -202,6 +202,19 @@ public class TestContextName {
Assert.assertEquals("/foo", cn22.getDisplayName());
}
+ @Test
+ public void testPathNormalisation() {
+ ContextName malformed = new ContextName("foo", null);
+ Assert.assertEquals("/foo", malformed.getPath());
+ Assert.assertEquals("/foo", malformed.getName());
+ Assert.assertEquals("foo", malformed.getBaseName());
+
+ ContextName malformedVersion = new ContextName("foo", "A");
+ Assert.assertEquals("/foo", malformedVersion.getPath());
+ Assert.assertEquals("/foo##A", malformedVersion.getName());
+ Assert.assertEquals("foo##A", malformedVersion.getBaseName());
+ }
+
@Test
public void testConstructorString() {
doTestConstructorString(cn1);
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org