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 3dbd6861b clamp size() int overflow in composite and multi-valued 
classes (#710)
3dbd6861b is described below

commit 3dbd6861bb32d2a41520514f5c2ded7b1a830245
Author: Naveed Khan <[email protected]>
AuthorDate: Fri Jul 10 14:33:26 2026 +0000

    clamp size() int overflow in composite and multi-valued classes (#710)
    
    CompositeCollection, CompositeSet and CompositeMap sum the sizes of
    their composited containers into an int, and AbstractMultiValuedMap and
    AbstractMultiSet sum value collection sizes and entry counts the same
    way, so a total past Integer.MAX_VALUE wraps negative and size()
    breaks the Collection contract. Sum into a long and clamp the return
    to Integer.MAX_VALUE. Follow-up to #705 covering the remaining
    int/long size mismatches.
---
 src/changes/changes.xml                            |  1 +
 .../collection/CompositeCollection.java            |  7 +++--
 .../commons/collections4/map/CompositeMap.java     |  4 +--
 .../multimap/AbstractMultiValuedMap.java           |  4 +--
 .../collections4/multiset/AbstractMultiSet.java    |  7 +++--
 .../commons/collections4/set/CompositeSet.java     |  7 +++--
 .../collection/CompositeCollectionTest.java        | 11 ++++++++
 .../commons/collections4/map/CompositeMapTest.java | 25 ++++++++++++++++
 .../multimap/AbstractMultiValuedMapTest.java       | 17 +++++++++++
 .../multiset/AbstractMultiSetTest.java             | 33 ++++++++++++++++++++++
 .../commons/collections4/set/CompositeSetTest.java | 26 +++++++++++++++++
 11 files changed, 129 insertions(+), 13 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 4efffe41a..b4b85fa88 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -25,6 +25,7 @@
   <release version="4.6.0" date="YYYY-MM-DD" description="This is a feature 
and maintenance release. Java 8 or later is required.">
     <!-- FIX -->
     <action type="fix" dev="ggregory" due-to="Naveed Khan, Gary 
Gregory">AbstractMapBag and AbstractMapMultiSet.add(Object, int) overflow the 
size and element count past Integer.MAX_VALUE.</action>
+    <action type="fix" dev="ggregory" due-to="Naveed 
Khan">CompositeCollection, CompositeSet, CompositeMap, AbstractMultiValuedMap 
and AbstractMultiSet size() overflow int when the total exceeds 
Integer.MAX_VALUE.</action>
     <action type="fix" dev="ggregory" due-to="Gary Gregory" 
issue="COLLECTIONS-776">Wrapping PassiveExpiringMap in 
Collections.synchronizedMap breaks expiration.</action>
     <action type="fix" dev="ggregory" due-to="Naveed Khan, Gary 
Gregory">[javadoc] Document that BloomFilter merge operations are non-atomic 
and a filter that throws during a merge should be considered invalid.</action>
     <action type="fix" dev="ggregory" due-to="Gary 
Gregory">org.apache.commons.collections4.map.AbstractReferenceMap.ReferenceEntry.toReference(ReferenceStrength,
 T, int) now throws IllegalArgumentException instead of Error.</action>
diff --git 
a/src/main/java/org/apache/commons/collections4/collection/CompositeCollection.java
 
b/src/main/java/org/apache/commons/collections4/collection/CompositeCollection.java
index 6623fae13..486e132b1 100644
--- 
a/src/main/java/org/apache/commons/collections4/collection/CompositeCollection.java
+++ 
b/src/main/java/org/apache/commons/collections4/collection/CompositeCollection.java
@@ -425,15 +425,16 @@ public class CompositeCollection<E> implements 
Collection<E>, Serializable {
      * This implementation calls {@code size()} on each collection.
      * </p>
      *
-     * @return total number of elements in all contained containers.
+     * @return total number of elements in all contained containers, or
+     *         {@code Integer.MAX_VALUE} if the total exceeds it.
      */
     @Override
     public int size() {
-        int size = 0;
+        long size = 0;
         for (final Collection<E> item : all) {
             size += item.size();
         }
-        return size;
+        return (int) Math.min(size, Integer.MAX_VALUE);
     }
 
     /**
diff --git 
a/src/main/java/org/apache/commons/collections4/map/CompositeMap.java 
b/src/main/java/org/apache/commons/collections4/map/CompositeMap.java
index 640bb7498..c23fd1bf9 100644
--- a/src/main/java/org/apache/commons/collections4/map/CompositeMap.java
+++ b/src/main/java/org/apache/commons/collections4/map/CompositeMap.java
@@ -536,11 +536,11 @@ public class CompositeMap<K, V> extends 
AbstractIterableMap<K, V> implements Ser
      */
     @Override
     public int size() {
-        int size = 0;
+        long size = 0;
         for (int i = composite.length - 1; i >= 0; --i) {
             size += composite[i].size();
         }
-        return size;
+        return (int) Math.min(size, Integer.MAX_VALUE);
     }
 
     /**
diff --git 
a/src/main/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMap.java
 
b/src/main/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMap.java
index f6e707c7e..373f68716 100644
--- 
a/src/main/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMap.java
+++ 
b/src/main/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMap.java
@@ -903,11 +903,11 @@ public abstract class AbstractMultiValuedMap<K, V> 
implements MultiValuedMap<K,
         // but this requires that all modifications of the multimap
         // (including the wrapped collections and entry/value
         // collections) are tracked.
-        int size = 0;
+        long size = 0;
         for (final Collection<V> col : getMap().values()) {
             size += col.size();
         }
-        return size;
+        return (int) Math.min(size, Integer.MAX_VALUE);
     }
 
     @Override
diff --git 
a/src/main/java/org/apache/commons/collections4/multiset/AbstractMultiSet.java 
b/src/main/java/org/apache/commons/collections4/multiset/AbstractMultiSet.java
index a340117c0..7de3788a5 100644
--- 
a/src/main/java/org/apache/commons/collections4/multiset/AbstractMultiSet.java
+++ 
b/src/main/java/org/apache/commons/collections4/multiset/AbstractMultiSet.java
@@ -475,15 +475,16 @@ public abstract class AbstractMultiSet<E> extends 
AbstractCollection<E> implemen
     /**
      * Returns the number of elements in this multiset.
      *
-     * @return current size of the multiset
+     * @return current size of the multiset, or {@code Integer.MAX_VALUE}
+     *         if the total exceeds it
      */
     @Override
     public int size() {
-        int totalSize = 0;
+        long totalSize = 0;
         for (final Entry<E> entry : entrySet()) {
             totalSize += entry.getCount();
         }
-        return totalSize;
+        return (int) Math.min(totalSize, Integer.MAX_VALUE);
     }
 
     /**
diff --git 
a/src/main/java/org/apache/commons/collections4/set/CompositeSet.java 
b/src/main/java/org/apache/commons/collections4/set/CompositeSet.java
index 5a640f5c1..290606e29 100644
--- a/src/main/java/org/apache/commons/collections4/set/CompositeSet.java
+++ b/src/main/java/org/apache/commons/collections4/set/CompositeSet.java
@@ -465,15 +465,16 @@ public class CompositeSet<E> implements Set<E>, 
Serializable {
      * <p>
      * This implementation calls {@code size()} on each set.
      *
-     * @return total number of elements in all contained containers
+     * @return total number of elements in all contained containers, or
+     *         {@code Integer.MAX_VALUE} if the total exceeds it
      */
     @Override
     public int size() {
-        int size = 0;
+        long size = 0;
         for (final Set<E> item : all) {
             size += item.size();
         }
-        return size;
+        return (int) Math.min(size, Integer.MAX_VALUE);
     }
 
     /**
diff --git 
a/src/test/java/org/apache/commons/collections4/collection/CompositeCollectionTest.java
 
b/src/test/java/org/apache/commons/collections4/collection/CompositeCollectionTest.java
index b7116edd7..3dacfb4ca 100644
--- 
a/src/test/java/org/apache/commons/collections4/collection/CompositeCollectionTest.java
+++ 
b/src/test/java/org/apache/commons/collections4/collection/CompositeCollectionTest.java
@@ -29,6 +29,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.function.Predicate;
 
+import org.apache.commons.collections4.bag.HashBag;
 import org.junit.jupiter.api.Test;
 
 /**
@@ -424,6 +425,16 @@ public class CompositeCollectionTest<E> extends 
AbstractCollectionTest<E> {
         assertEquals(set.size(), c.size());
     }
 
+    @Test
+    void testSizeClampsToIntegerMaxValue() {
+        final HashBag<String> one = new HashBag<>();
+        one.add("A", Integer.MAX_VALUE);
+        final HashBag<String> two = new HashBag<>();
+        two.add("B", Integer.MAX_VALUE);
+        final CompositeCollection<String> composite = new 
CompositeCollection<>(one, two);
+        assertEquals(Integer.MAX_VALUE, composite.size());
+    }
+
     @Test
     @SuppressWarnings("unchecked")
     void testToCollection() {
diff --git 
a/src/test/java/org/apache/commons/collections4/map/CompositeMapTest.java 
b/src/test/java/org/apache/commons/collections4/map/CompositeMapTest.java
index c33bb6d94..e315788c0 100644
--- a/src/test/java/org/apache/commons/collections4/map/CompositeMapTest.java
+++ b/src/test/java/org/apache/commons/collections4/map/CompositeMapTest.java
@@ -21,9 +21,12 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.util.AbstractMap;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
@@ -235,6 +238,28 @@ public class CompositeMapTest<K, V> extends 
AbstractIterableMapTest<K, V> {
         assertTrue(pass);
     }
 
+    @Test
+    void testSizeClampsToIntegerMaxValue() {
+        final CompositeMap<String, String> map = new 
CompositeMap<>(maxSizeMap(), maxSizeMap());
+        assertEquals(Integer.MAX_VALUE, map.size());
+    }
+
+    /** An empty-iterating map that reports {@code Integer.MAX_VALUE} 
mappings. */
+    private static Map<String, String> maxSizeMap() {
+        return new AbstractMap<String, String>() {
+
+            @Override
+            public Set<Map.Entry<String, String>> entrySet() {
+                return Collections.emptySet();
+            }
+
+            @Override
+            public int size() {
+                return Integer.MAX_VALUE;
+            }
+        };
+    }
+
 //    void testCreate() throws Exception {
 //        resetEmpty();
 //        writeExternalFormToDisk((java.io.Serializable) map, 
"src/test/resources/data/test/CompositeMap.emptyCollection.version4.obj");
diff --git 
a/src/test/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMapTest.java
 
b/src/test/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMapTest.java
index f0f670575..46588a3d1 100644
--- 
a/src/test/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMapTest.java
+++ 
b/src/test/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMapTest.java
@@ -1279,6 +1279,23 @@ public abstract class AbstractMultiValuedMapTest<K, V> 
extends AbstractObjectTes
         assertEquals(getSampleTotalValueCount(), makeFullMap().size());
     }
 
+    @Test
+    void testSizeClampsToIntegerMaxValue() {
+        // bag-valued map: value collection sizes are counts, so the total is 
cheap to grow past Integer.MAX_VALUE
+        final AbstractMultiValuedMap<String, String> map = new 
AbstractMultiValuedMap<String, String>(new HashMap<>()) {
+
+            @Override
+            protected Collection<String> createCollection() {
+                return new HashBag<>();
+            }
+        };
+        map.put("k1", "v1");
+        map.put("k2", "v2");
+        ((HashBag<String>) map.getMap().get("k1")).add("v1", Integer.MAX_VALUE 
- 1);
+        ((HashBag<String>) map.getMap().get("k2")).add("v2", Integer.MAX_VALUE 
- 1);
+        assertEquals(Integer.MAX_VALUE, map.size());
+    }
+
     @Test
     @SuppressWarnings("unchecked")
     void testSize_Key() {
diff --git 
a/src/test/java/org/apache/commons/collections4/multiset/AbstractMultiSetTest.java
 
b/src/test/java/org/apache/commons/collections4/multiset/AbstractMultiSetTest.java
index 90b5fa1b8..a3b52d251 100644
--- 
a/src/test/java/org/apache/commons/collections4/multiset/AbstractMultiSetTest.java
+++ 
b/src/test/java/org/apache/commons/collections4/multiset/AbstractMultiSetTest.java
@@ -691,6 +691,39 @@ public abstract class AbstractMultiSetTest<T> extends 
AbstractCollectionTest<T>
         assertEquals(2, multiset.size(), "Should have 2 total item");
     }
 
+    @Test
+    void testMultiSetSizeClampsToIntegerMaxValue() {
+        final List<MultiSet.Entry<String>> entries = 
Arrays.asList(maxCountEntry("A"), maxCountEntry("B"));
+        final MultiSet<String> multiset = new AbstractMultiSet<String>() {
+
+            @Override
+            protected Iterator<MultiSet.Entry<String>> 
createEntrySetIterator() {
+                return entries.iterator();
+            }
+
+            @Override
+            protected int uniqueElements() {
+                return entries.size();
+            }
+        };
+        assertEquals(Integer.MAX_VALUE, multiset.size());
+    }
+
+    private static MultiSet.Entry<String> maxCountEntry(final String element) {
+        return new AbstractMultiSet.AbstractEntry<String>() {
+
+            @Override
+            public int getCount() {
+                return Integer.MAX_VALUE;
+            }
+
+            @Override
+            public String getElement() {
+                return element;
+            }
+        };
+    }
+
     @Test
     @SuppressWarnings("unchecked")
     void testMultiSetToArray() {
diff --git 
a/src/test/java/org/apache/commons/collections4/set/CompositeSetTest.java 
b/src/test/java/org/apache/commons/collections4/set/CompositeSetTest.java
index 1951338a1..0a497bb2d 100644
--- a/src/test/java/org/apache/commons/collections4/set/CompositeSetTest.java
+++ b/src/test/java/org/apache/commons/collections4/set/CompositeSetTest.java
@@ -21,8 +21,11 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.util.AbstractSet;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 
@@ -180,6 +183,29 @@ public class CompositeSetTest<E> extends 
AbstractSetTest<E> {
         assertFalse(one.contains("3"));
     }
 
+    @Test
+    void testSizeClampsToIntegerMaxValue() {
+        final CompositeSet<String> set = new CompositeSet<>(maxSizeSet());
+        set.addComposited(maxSizeSet());
+        assertEquals(Integer.MAX_VALUE, set.size());
+    }
+
+    /** An empty-iterating set that reports {@code Integer.MAX_VALUE} 
elements. */
+    private static Set<String> maxSizeSet() {
+        return new AbstractSet<String>() {
+
+            @Override
+            public Iterator<String> iterator() {
+                return Collections.emptyIterator();
+            }
+
+            @Override
+            public int size() {
+                return Integer.MAX_VALUE;
+            }
+        };
+    }
+
     @Test
     @SuppressWarnings("unchecked")
     void testRemoveUnderlying() {

Reply via email to