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

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-collections.git


The following commit(s) were added to refs/heads/master by this push:
     new 3099308e4 [COLLECTIONS-714] PatriciaTrie ignores trailing null 
characters in keys (#712)
3099308e4 is described below

commit 3099308e4bb0155da6e8f4ea0d0455e06985a91a
Author: sankalp suman <[email protected]>
AuthorDate: Thu Jul 16 02:47:04 2026 +0530

    [COLLECTIONS-714] PatriciaTrie ignores trailing null characters in keys 
(#712)
    
    * COLLECTIONS-732: Document UnsupportedOperationException on 
MultiValuedMap.mapIterator().setValue()
    
    * COLLECTIONS-732: Fix Javadoc on 
AbstractMultiValuedMapDecorator.mapIterator
    
    * [COLLECTIONS-714]Fix StringKeyAnalyzer bitIndex and isBitSet for keys 
containing null characters
    
    * COLLECTIONS-714: Add regression tests for Java UTF-16 control characters 
in PatriciaTrie
    
    * COLLECTIONS-714: Remove useless parentheses in StringKeyAnalyzer to fix 
PMD violation
    
    * Fix formatting in StringKeyAnalyzer.java
    
    * Remove bogus Javadoc
    
    Removed unsupported operation documentation from setValue method.
    
    * Remove bogus Javadoc.
    
    Removed unsupported operation note from mapIterator method documentation.
    
    * Remove unsupported operation comment from mapIterator
    
    Removed unsupported operation documentation from mapIterator method.
    
    ---------
    
    Co-authored-by: Gary Gregory <[email protected]>
---
 .../multimap/UnmodifiableMultiValuedMap.java       |   7 ++
 .../trie/analyzer/StringKeyAnalyzer.java           |  47 +++-----
 .../collections4/trie/PatriciaTrieTest.java        | 121 ++++++++++++++++++++-
 3 files changed, 144 insertions(+), 31 deletions(-)

diff --git 
a/src/main/java/org/apache/commons/collections4/multimap/UnmodifiableMultiValuedMap.java
 
b/src/main/java/org/apache/commons/collections4/multimap/UnmodifiableMultiValuedMap.java
index 90b8c780d..74e81cb0c 100644
--- 
a/src/main/java/org/apache/commons/collections4/multimap/UnmodifiableMultiValuedMap.java
+++ 
b/src/main/java/org/apache/commons/collections4/multimap/UnmodifiableMultiValuedMap.java
@@ -114,6 +114,13 @@ public final class UnmodifiableMultiValuedMap<K, V>
         return UnmodifiableSet.unmodifiableSet(decorated().keySet());
     }
 
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The returned map iterator's {@link MapIterator#setValue(Object)} method 
is not supported
+     * and will throw an {@link UnsupportedOperationException}.
+     * </p>
+     */
     @Override
     public MapIterator<K, V> mapIterator() {
         return 
UnmodifiableMapIterator.unmodifiableMapIterator(decorated().mapIterator());
diff --git 
a/src/main/java/org/apache/commons/collections4/trie/analyzer/StringKeyAnalyzer.java
 
b/src/main/java/org/apache/commons/collections4/trie/analyzer/StringKeyAnalyzer.java
index 6b0aaa6df..f28c98d14 100644
--- 
a/src/main/java/org/apache/commons/collections4/trie/analyzer/StringKeyAnalyzer.java
+++ 
b/src/main/java/org/apache/commons/collections4/trie/analyzer/StringKeyAnalyzer.java
@@ -33,8 +33,8 @@ public class StringKeyAnalyzer extends KeyAnalyzer<String> {
     /** A singleton instance of {@link StringKeyAnalyzer}. */
     public static final StringKeyAnalyzer INSTANCE = new StringKeyAnalyzer();
 
-    /** The number of bits per {@link Character}. */
-    public static final int LENGTH = Character.SIZE;
+    /** The number of bits per {@link Character} plus a presence bit. */
+    public static final int LENGTH = Character.SIZE + 1;
 
     /** A bit mask where the first bit is 1 and the others are zero. */
     private static final int MSB = 0x8000;
@@ -58,8 +58,6 @@ public class StringKeyAnalyzer extends KeyAnalyzer<String> {
     public int bitIndex(final String key, final int offsetInBits, final int 
lengthInBits,
                         final String other, final int otherOffsetInBits, final 
int otherLengthInBits) {
 
-        boolean allNull = true;
-
         if (offsetInBits % LENGTH != 0 || otherOffsetInBits % LENGTH != 0
                 || lengthInBits % LENGTH != 0 || otherLengthInBits % LENGTH != 
0) {
             throw new IllegalArgumentException("The offsets and lengths must 
be at Character boundaries");
@@ -69,43 +67,29 @@ public class StringKeyAnalyzer extends KeyAnalyzer<String> {
         final int beginIndex2 = otherOffsetInBits / LENGTH;
 
         final int endIndex1 = beginIndex1 + lengthInBits / LENGTH;
-        final int endIndex2 = beginIndex2 + otherLengthInBits / LENGTH;
+        final int endIndex2 = other == null ? beginIndex2 : beginIndex2 + 
otherLengthInBits / LENGTH;
 
         final int length = Math.max(endIndex1, endIndex2);
 
-        // Look at each character, and if they're different
-        // then figure out which bit makes the difference
-        // and return it.
-        char k = 0;
-        char f = 0;
         for (int i = 0; i < length; i++) {
             final int index1 = beginIndex1 + i;
             final int index2 = beginIndex2 + i;
 
-            if (index1 >= endIndex1) {
-                k = 0;
-            } else {
-                k = key.charAt(index1);
-            }
+            if (index1 < endIndex1 && other != null && index2 < endIndex2) {
+                final char k = key.charAt(index1);
+                final char f = other.charAt(index2);
 
-            if (other == null || index2 >= endIndex2) {
-                f = 0;
+                if (k != f) {
+                    final int x = k ^ f;
+                    return i * LENGTH + 1 + Integer.numberOfLeadingZeros(x) - 
(LENGTH - 1);
+                }
             } else {
-                f = other.charAt(index2);
-            }
-
-            if (k != f) {
-                final int x = k ^ f;
-                return i * LENGTH + Integer.numberOfLeadingZeros(x) - LENGTH;
-            }
-
-            if (k != 0) {
-                allNull = false;
+                // One has ended, the other has not. They differ at the 
presence bit of this block.
+                return i * LENGTH;
             }
         }
 
-        // All bits are 0
-        if (allNull) {
+        if (lengthInBits == 0 && (other == null || otherLengthInBits == 0)) {
             return NULL_BIT_KEY;
         }
 
@@ -127,7 +111,10 @@ public class StringKeyAnalyzer extends KeyAnalyzer<String> 
{
         final int index = bitIndex / LENGTH;
         final int bit = bitIndex % LENGTH;
 
-        return (key.charAt(index) & mask(bit)) != 0;
+        if (bit == 0) {
+            return true;
+        }
+        return (key.charAt(index) & mask(bit - 1)) != 0;
     }
 
     @Override
diff --git 
a/src/test/java/org/apache/commons/collections4/trie/PatriciaTrieTest.java 
b/src/test/java/org/apache/commons/collections4/trie/PatriciaTrieTest.java
index ad2c73084..46f1bef91 100644
--- a/src/test/java/org/apache/commons/collections4/trie/PatriciaTrieTest.java
+++ b/src/test/java/org/apache/commons/collections4/trie/PatriciaTrieTest.java
@@ -667,6 +667,126 @@ public class PatriciaTrieTest<V> extends 
AbstractSortedMapTest<String, V> {
         assertEquals("ge", tailMap.get("ge"));
     }
 
+    @Test
+    void testNullTerminatedKey1() {
+        final PatriciaTrie<Integer> trie = new PatriciaTrie<>();
+        trie.put("x", 0);
+        trie.put("x\u0000", 1);
+        assertTrue(trie.containsKey("x"));
+        assertTrue(trie.containsKey("x\u0000"));
+        assertEquals(2, trie.size());
+        assertEquals(0, trie.get("x"));
+        assertEquals(1, trie.get("x\u0000"));
+    }
+
+    @Test
+    void testNullTerminatedKey2() {
+        final PatriciaTrie<Integer> trie = new PatriciaTrie<>();
+        trie.put("x\u0000", 1);
+        trie.put("x", 0);
+        assertTrue(trie.containsKey("x"));
+        assertTrue(trie.containsKey("x\u0000"));
+        assertEquals(2, trie.size());
+        assertEquals(0, trie.get("x"));
+        assertEquals(1, trie.get("x\u0000"));
+    }
+
+    @Test
+    public void testControlCharactersRegression() {
+        final String[] keys = {
+            "",
+            "\u0000",
+            "\u0001",
+            "\u0002",
+            "\u001F",
+            "\u007F",
+            "x",
+            "x\u0000",
+            "x\u0001",
+            "x\u0002",
+            "x\u001F",
+            "x\u007F",
+            "x\u0000y",
+            "x\u0001y"
+        };
+
+        // 1. Insertion, Retrieval, containsKey, size compared with HashMap
+        final java.util.Map<String, String> hashMap = new 
java.util.HashMap<>();
+        final PatriciaTrie<String> trie = new PatriciaTrie<>();
+
+        for (int i = 0; i < keys.length; i++) {
+            final String key = keys[i];
+            final String value = "val_" + i;
+
+            assertFalse(trie.containsKey(key));
+            assertNull(trie.get(key));
+
+            trie.put(key, value);
+            hashMap.put(key, value);
+
+            assertEquals(hashMap.size(), trie.size());
+            assertTrue(trie.containsKey(key));
+            assertEquals(value, trie.get(key));
+        }
+
+        // Verify all elements present and distinct
+        for (int i = 0; i < keys.length; i++) {
+            final String key = keys[i];
+            assertTrue(trie.containsKey(key));
+            assertEquals("val_" + i, trie.get(key));
+        }
+
+        // 2. Iteration order (compared with TreeMap)
+        final java.util.TreeMap<String, String> treeMap = new 
java.util.TreeMap<>(hashMap);
+        final java.util.List<String> trieKeys = new 
java.util.ArrayList<>(trie.keySet());
+        final java.util.List<String> expectedKeys = new 
java.util.ArrayList<>(treeMap.keySet());
+        assertEquals(expectedKeys, trieKeys);
+
+        // 3. prefixMap
+        final String[] prefixes = {"", "x", "x\u0000", "x\u0001", "x\u0002", 
"x\u001F", "x\u007F", "y"};
+        for (final String prefix : prefixes) {
+            final java.util.SortedMap<String, String> prefixMap = 
trie.prefixMap(prefix);
+            final java.util.TreeMap<String, String> expectedPrefixMap = new 
java.util.TreeMap<>();
+            for (final java.util.Map.Entry<String, String> entry : 
treeMap.entrySet()) {
+                if (entry.getKey().startsWith(prefix)) {
+                    expectedPrefixMap.put(entry.getKey(), entry.getValue());
+                }
+            }
+            assertEquals(expectedPrefixMap.size(), prefixMap.size());
+            assertEquals(expectedPrefixMap, new 
java.util.TreeMap<>(prefixMap));
+        }
+
+        // 4. headMap, tailMap, subMap (compared with TreeMap)
+        for (final String k1 : keys) {
+            assertEquals(new java.util.TreeMap<>(treeMap.headMap(k1)), new 
java.util.TreeMap<>(trie.headMap(k1)));
+            assertEquals(new java.util.TreeMap<>(treeMap.tailMap(k1)), new 
java.util.TreeMap<>(trie.tailMap(k1)));
+            for (final String k2 : keys) {
+                if (k1.compareTo(k2) <= 0) {
+                    assertEquals(new java.util.TreeMap<>(treeMap.subMap(k1, 
k2)), new java.util.TreeMap<>(trie.subMap(k1, k2)));
+                } else {
+                    assertThrows(IllegalArgumentException.class, () -> 
trie.subMap(k1, k2));
+                }
+            }
+        }
+
+        // 5. Remove (compared with HashMap)
+        for (int i = 0; i < keys.length; i++) {
+            final String key = keys[i];
+            final String value = "val_" + i;
+
+            assertTrue(trie.containsKey(key));
+            assertEquals(value, trie.remove(key));
+            assertFalse(trie.containsKey(key));
+            assertNull(trie.get(key));
+
+            hashMap.remove(key);
+            assertEquals(hashMap.size(), trie.size());
+        }
+
+        assertTrue(trie.isEmpty());
+        assertEquals(0, trie.size());
+    }
+
 //    void testCreate() throws Exception {
 //        resetEmpty();
 //        writeExternalFormToDisk(
@@ -677,5 +797,4 @@ public class PatriciaTrieTest<V> extends 
AbstractSortedMapTest<String, V> {
 //            (java.io.Serializable) map,
 //            
"src/test/resources/data/test/PatriciaTrie.fullCollection.version4.obj");
 //    }
-
 }

Reply via email to