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

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

commit a12bae3868c2de799e6a0c46219e52394148831f
Author: opencode <[email protected]>
AuthorDate: Wed Oct 14 12:10:12 2026 +0200

    Make CaseInsensitiveKeyMap.keySet() view fully case-insensitive
    
    The class extends AbstractMap and only overrode entrySet(), so keySet()
    returned the AbstractMap view whose contains() delegates to the map's
    case-insensitive containsKey() while remove(Object) iterates the keys
    comparing them with String.equals(). For a stored key "a",
    keySet().contains("A") returned true but keySet().remove("A")
    returned false and left the entry in place, violating the Set contract
    for callers that remove keys via the view with different casing.
    
    Override keySet() with a KeySet view backed by the same live map as the
    entry set. Its iterator reports keys in their original case, contains()
    and remove() are case-insensitive, removal correctly reports success
    even for entries with a null value, null and non-String arguments return
    false, and clear() empties the backing map. Non-String and null keys are
    never stored since put() rejects null keys and Key.getInstance() returns
    null for other types.
    
    Add tests for case-insensitive removal of a set object, rejection of
    null and non-String keys, original-case keys from the iterator and
    clearing via the view.
---
 .../util/collections/CaseInsensitiveKeyMap.java    | 74 ++++++++++++++++++++++
 .../collections/TestCaseInsensitiveKeyMap.java     | 60 ++++++++++++++++++
 2 files changed, 134 insertions(+)

diff --git a/java/org/apache/tomcat/util/collections/CaseInsensitiveKeyMap.java 
b/java/org/apache/tomcat/util/collections/CaseInsensitiveKeyMap.java
index ef125ecc39..c5cb2fa80a 100644
--- a/java/org/apache/tomcat/util/collections/CaseInsensitiveKeyMap.java
+++ b/java/org/apache/tomcat/util/collections/CaseInsensitiveKeyMap.java
@@ -94,6 +94,80 @@ public class CaseInsensitiveKeyMap<V> extends 
AbstractMap<String,V> {
     }
 
 
+    /*
+     * The AbstractMap.keySet() implementation cannot be used since its 
contains() method is case-insensitive (it
+     * delegates to containsKey()) while its remove() method is case-sensitive 
(it iterates the keys comparing them
+     * using String.equals()). That violates the Set contract for keys that 
differ in case. This view keeps all
+     * operations case-insensitive.
+     */
+    @Override
+    public Set<String> keySet() {
+        return new KeySet<>(map);
+    }
+
+
+    private static class KeySet<V> extends AbstractSet<String> {
+
+        private final Map<Key,V> map;
+
+        KeySet(Map<Key,V> map) {
+            this.map = map;
+        }
+
+        @Override
+        public Iterator<String> iterator() {
+            return new KeyIterator(map.keySet().iterator());
+        }
+
+        @Override
+        public int size() {
+            return map.size();
+        }
+
+        @Override
+        public boolean contains(Object o) {
+            return map.containsKey(Key.getInstance(o));
+        }
+
+        @Override
+        public boolean remove(Object o) {
+            Key key = Key.getInstance(o);
+            if (key == null) {
+                return false;
+            }
+            if (!map.containsKey(key)) {
+                return false;
+            }
+            map.remove(key);
+            return true;
+        }
+
+        @Override
+        public void clear() {
+            map.clear();
+        }
+    }
+
+
+    private record KeyIterator(Iterator<Key> iterator) implements 
Iterator<String> {
+
+        @Override
+        public boolean hasNext() {
+            return iterator.hasNext();
+        }
+
+        @Override
+        public String next() {
+            return iterator.next().getKey();
+        }
+
+        @Override
+        public void remove() {
+            iterator.remove();
+        }
+    }
+
+
     private static class EntrySet<V> extends AbstractSet<Entry<String,V>> {
 
         private final Set<Entry<Key,V>> entrySet;
diff --git 
a/test/org/apache/tomcat/util/collections/TestCaseInsensitiveKeyMap.java 
b/test/org/apache/tomcat/util/collections/TestCaseInsensitiveKeyMap.java
index c094bfa58b..44be5891da 100644
--- a/test/org/apache/tomcat/util/collections/TestCaseInsensitiveKeyMap.java
+++ b/test/org/apache/tomcat/util/collections/TestCaseInsensitiveKeyMap.java
@@ -197,6 +197,66 @@ public class TestCaseInsensitiveKeyMap {
     }
 
 
+    @Test
+    public void testKeySetRemoveObjectCaseInsensitive() {
+        Object o1 = new Object();
+
+        CaseInsensitiveKeyMap<Object> map = new CaseInsensitiveKeyMap<>();
+        map.put("a", o1);
+
+        // contains() is case-insensitive, so remove() must be too, even for
+        // a stored value of null
+        Set<String> keys = map.keySet();
+        Assert.assertTrue(keys.contains("A"));
+        map.put("b", null);
+        Assert.assertTrue(keys.remove("B"));
+        Assert.assertFalse(map.containsKey("b"));
+
+        Assert.assertTrue(keys.remove("A"));
+        Assert.assertFalse(map.containsKey("a"));
+        Assert.assertTrue(map.isEmpty());
+
+        // Removing an absent key must not report success
+        Assert.assertFalse(keys.remove("A"));
+    }
+
+
+    @Test
+    public void testKeySetContainsAndRemoveInvalidKeys() {
+        CaseInsensitiveKeyMap<Object> map = new CaseInsensitiveKeyMap<>();
+        map.put("a", new Object());
+
+        Set<String> keys = map.keySet();
+        Assert.assertFalse(keys.contains(null));
+        Assert.assertFalse(keys.remove(null));
+        Assert.assertFalse(keys.contains(new StringBuilder("a")));
+        Assert.assertFalse(keys.remove(new StringBuilder("a")));
+        Assert.assertEquals(1, map.size());
+    }
+
+
+    @Test
+    public void testKeySetIteratorCase() {
+        CaseInsensitiveKeyMap<Object> map = new CaseInsensitiveKeyMap<>();
+        map.put("Content-Type", new Object());
+
+        // The keys are reported with their original case
+        Assert.assertTrue(map.keySet().contains("Content-Type"));
+        Assert.assertEquals("Content-Type", map.keySet().iterator().next());
+    }
+
+
+    @Test
+    public void testKeySetClear() {
+        CaseInsensitiveKeyMap<Object> map = new CaseInsensitiveKeyMap<>();
+        map.put("a", new Object());
+        map.put("b", new Object());
+
+        map.keySet().clear();
+        Assert.assertTrue(map.isEmpty());
+    }
+
+
     @Test
     public void testEntrySetRemove() {
         Object o1 = new Object();


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

Reply via email to