This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 58eac76d9bd610a721cef94e4cb32bcc8a49e76c
Author: opencode <[email protected]>
AuthorDate: Sun Oct 18 12:45:43 2026 +0200

    Correct equals() implementations in method expression classes
    
    MethodExpressionImpl.equals() and MethodExpressionLiteral.equals()
    previously decided equality by comparing only the hashCode() of the
    expression string. Distinct expressions whose expression strings
    happened to have colliding 32-bit hash codes were therefore reported
    as equal, violating the equals() contract and the documented
    jakarta.el.Expression "identical parsed representation" semantics.
    
    MethodExpressionImpl now mirrors ValueExpressionImpl: equals() uses
    the hash code only as a fast path filter and then compares the parsed
    AST nodes (SimpleNode provides semantic equals()/hashCode() based on
    node type, image and children), with hashCode() derived from the
    parsed node so the equals()/hashCode() contract holds, including for
    expressions that differ only in whitespace. MethodExpressionLiteral
    now compares the literal expression string directly.
    
    Adds unit tests covering identical expressions, whitespace variants,
    different expressions and the classic "Aa"/"BB" string hash-collision
    pair for both classes.
---
 java/org/apache/el/MethodExpressionImpl.java     | 11 +++-
 java/org/apache/el/MethodExpressionLiteral.java  |  6 ++-
 test/org/apache/el/TestMethodExpressionImpl.java | 69 ++++++++++++++++++++++++
 3 files changed, 83 insertions(+), 3 deletions(-)

diff --git a/java/org/apache/el/MethodExpressionImpl.java 
b/java/org/apache/el/MethodExpressionImpl.java
index 208c22a714..83a0a9ae68 100644
--- a/java/org/apache/el/MethodExpressionImpl.java
+++ b/java/org/apache/el/MethodExpressionImpl.java
@@ -127,7 +127,14 @@ public final class MethodExpressionImpl extends 
MethodExpression implements Exte
      */
     @Override
     public boolean equals(Object obj) {
-        return (obj instanceof MethodExpressionImpl && obj.hashCode() == 
this.hashCode());
+        if (!(obj instanceof MethodExpressionImpl)) {
+            return false;
+        }
+        if (obj.hashCode() != this.hashCode()) {
+            return false;
+        }
+
+        return this.getNode().equals(((MethodExpressionImpl) obj).getNode());
     }
 
     /**
@@ -206,7 +213,7 @@ public final class MethodExpressionImpl extends 
MethodExpression implements Exte
      */
     @Override
     public int hashCode() {
-        return this.expr.hashCode();
+        return this.getNode().hashCode();
     }
 
     /**
diff --git a/java/org/apache/el/MethodExpressionLiteral.java 
b/java/org/apache/el/MethodExpressionLiteral.java
index 6606ec9627..497ab083dd 100644
--- a/java/org/apache/el/MethodExpressionLiteral.java
+++ b/java/org/apache/el/MethodExpressionLiteral.java
@@ -87,7 +87,11 @@ public class MethodExpressionLiteral extends 
MethodExpression implements Externa
 
     @Override
     public boolean equals(Object obj) {
-        return (obj instanceof MethodExpressionLiteral && this.hashCode() == 
obj.hashCode());
+        if (!(obj instanceof MethodExpressionLiteral)) {
+            return false;
+        }
+
+        return this.expr.equals(((MethodExpressionLiteral) obj).expr);
     }
 
     @Override
diff --git a/test/org/apache/el/TestMethodExpressionImpl.java 
b/test/org/apache/el/TestMethodExpressionImpl.java
index b47746abb6..cc5cb11186 100644
--- a/test/org/apache/el/TestMethodExpressionImpl.java
+++ b/test/org/apache/el/TestMethodExpressionImpl.java
@@ -744,4 +744,73 @@ public class TestMethodExpressionImpl {
         Assert.assertEquals(1, mi.getParamTypes().length);
         Assert.assertEquals(String.class, mi.getParamTypes()[0]);
     }
+
+
+    @Test
+    public void testEqualsHashCodeSameExpression() {
+        MethodExpression me1 = factory.createMethodExpression(context, 
"${beanB.getName}", String.class,
+                new Class<?>[] {});
+        MethodExpression me2 = factory.createMethodExpression(context, 
"${beanB.getName}", String.class,
+                new Class<?>[] {});
+
+        Assert.assertEquals(me1, me2);
+        Assert.assertEquals(me1.hashCode(), me2.hashCode());
+    }
+
+
+    @Test
+    public void testEqualsHashCodeWhitespaceVariants() {
+        MethodExpression me1 = factory.createMethodExpression(context, 
"${beanB.getName}", String.class,
+                new Class<?>[] {});
+        MethodExpression me2 = factory.createMethodExpression(context, "${ 
beanB.getName }", String.class,
+                new Class<?>[] {});
+
+        Assert.assertEquals(me1, me2);
+        Assert.assertEquals(me1.hashCode(), me2.hashCode());
+    }
+
+
+    @Test
+    public void testNotEqualsDifferentExpression() {
+        MethodExpression me1 = factory.createMethodExpression(context, 
"${beanB.getName}", String.class,
+                new Class<?>[] {});
+        MethodExpression me2 = factory.createMethodExpression(context, 
"${beanBB.getName}", String.class,
+                new Class<?>[] {});
+
+        Assert.assertNotEquals(me1, me2);
+    }
+
+
+    @Test
+    public void testNotEqualsHashCollidingExpressions() {
+        // "Aa" and "BB" are a well known pair of strings with the same hash 
code
+        Assert.assertEquals("Aa".hashCode(), "BB".hashCode());
+
+        MethodExpression me1 = factory.createMethodExpression(context, 
"${Aa}", String.class, new Class<?>[] {});
+        MethodExpression me2 = factory.createMethodExpression(context, 
"${BB}", String.class, new Class<?>[] {});
+
+        Assert.assertNotEquals(me1, me2);
+    }
+
+
+    @Test
+    public void testEqualsHashCodeSameLiteral() {
+        MethodExpression me1 = factory.createMethodExpression(context, "'Aa'", 
String.class, new Class<?>[] {});
+        MethodExpression me2 = factory.createMethodExpression(context, "'Aa'", 
String.class, new Class<?>[] {});
+
+        Assert.assertEquals(me1, me2);
+        Assert.assertEquals(me1.hashCode(), me2.hashCode());
+    }
+
+
+    @Test
+    public void testNotEqualsHashCollidingLiterals() {
+        // "Aa" and "BB" are a well known pair of strings with the same hash 
code
+        Assert.assertEquals("'Aa'".hashCode(), "'BB'".hashCode());
+
+        MethodExpression me1 = factory.createMethodExpression(context, "'Aa'", 
String.class, new Class<?>[] {});
+        MethodExpression me2 = factory.createMethodExpression(context, "'BB'", 
String.class, new Class<?>[] {});
+
+        Assert.assertNotEquals(me1, me2);
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to