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>&amp;&amp;</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>&amp;&amp;</code>
+        and <code>||</code> distinct precedences, <code>&amp;&amp;</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 &amp;&amp; $c</code> was evaluated as
+        <code>($a || $b) &amp;&amp; $c</code> rather than the expected
+        <code>$a || ($b &amp;&amp; $c)</code>. Note that this changes the
+        evaluation of existing documents using unparenthesized mixed
+        <code>&amp;&amp;</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>&lt;</code>, <code>&gt;</code>,
+<code>&lt;=</code>, <code>&gt;=</code>), <code>&amp;&amp;</code> and finally
+<code>||</code>. For example, <code>$a || $b &amp;&amp; $c</code> is evaluated as
+<code>$a || ($b &amp;&amp; $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