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 927ddbe169c597e4545254ebcbea26035246389a 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 48c3d18849..45bf9f9886 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]
