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 fb2b16456 Clamp overflow in AbstractMapBag and AbstractMapMultiSet add 
(#705)
fb2b16456 is described below

commit fb2b164569a7daf193b40542a2705fc59ebfed5a
Author: Naveed Khan <[email protected]>
AuthorDate: Fri Jul 10 12:53:44 2026 +0000

    Clamp overflow in AbstractMapBag and AbstractMapMultiSet add (#705)
    
    * clamp overflow in AbstractMapBag and AbstractMapMultiSet add
    
    add(Object, int) accumulated size and the element count into int fields 
with no cap, so adding past Integer.MAX_VALUE overflowed and size()/getCount() 
returned negative values. Clamp both to Integer.MAX_VALUE.
    
    * track exact size in a long and clamp on return in size()
    
    clamping the stored size field lost information once the true size
    crossed Integer.MAX_VALUE: later removals dropped size() below
    Integer.MAX_VALUE while the real cardinality was still above it. keep
    the exact total in a long, saturate only the per-element count (stored
    as an int), accrue only the applied delta so size always equals the sum
    of counts, and clamp on return in size(). extends the regression tests
    to cover removal while the true size still exceeds Integer.MAX_VALUE.
---
 src/changes/changes.xml                              |  1 +
 .../commons/collections4/bag/AbstractMapBag.java     | 17 +++++++++++------
 .../collections4/multiset/AbstractMapMultiSet.java   | 15 +++++++++------
 .../apache/commons/collections4/bag/HashBagTest.java | 20 ++++++++++++++++++++
 .../collections4/multiset/HashMultiSetTest.java      | 18 ++++++++++++++++++
 5 files changed, 59 insertions(+), 12 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 17d574879..02a83f518 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -24,6 +24,7 @@
   <body>
   <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="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/bag/AbstractMapBag.java 
b/src/main/java/org/apache/commons/collections4/bag/AbstractMapBag.java
index ddbd039f1..6a424b6c5 100644
--- a/src/main/java/org/apache/commons/collections4/bag/AbstractMapBag.java
+++ b/src/main/java/org/apache/commons/collections4/bag/AbstractMapBag.java
@@ -146,8 +146,8 @@ public abstract class AbstractMapBag<E> implements Bag<E> {
     /** The map to use to store the data */
     private transient Map<E, MutableInteger> map;
 
-    /** The current total size of the bag */
-    private int size;
+    /** The current total size of the bag; kept exact past {@link 
Integer#MAX_VALUE}, {@link #size()} saturates */
+    private long size;
 
     /** The modification count for fail fast iterators */
     private transient int modCount;
@@ -196,6 +196,8 @@ public abstract class AbstractMapBag<E> implements Bag<E> {
 
     /**
      * Adds a new element to the bag, incrementing its count in the map.
+     * The count of an element saturates at {@code Integer.MAX_VALUE}; copies
+     * that would take it past that limit are not added.
      *
      * @param object the object to search for
      * @param nCopies the number of copies to add
@@ -206,12 +208,14 @@ public abstract class AbstractMapBag<E> implements Bag<E> 
{
         modCount++;
         if (nCopies > 0) {
             final MutableInteger mut = map.get(object);
-            size += nCopies;
             if (mut == null) {
                 map.put(object, new MutableInteger(nCopies));
+                size += nCopies;
                 return true;
             }
-            mut.value += nCopies;
+            final int applied = Math.min(nCopies, Integer.MAX_VALUE - 
mut.value);
+            mut.value += applied;
+            size += applied;
         }
         return false;
     }
@@ -523,13 +527,14 @@ public abstract class AbstractMapBag<E> implements Bag<E> 
{
     }
 
     /**
-     * Returns the number of elements in this bag.
+     * Returns the number of elements in this bag, or {@code Integer.MAX_VALUE}
+     * if the bag contains more than {@code Integer.MAX_VALUE} elements.
      *
      * @return current size of the bag
      */
     @Override
     public int size() {
-        return size;
+        return (int) Math.min(size, Integer.MAX_VALUE);
     }
 
     /**
diff --git 
a/src/main/java/org/apache/commons/collections4/multiset/AbstractMapMultiSet.java
 
b/src/main/java/org/apache/commons/collections4/multiset/AbstractMapMultiSet.java
index 4d62bc47a..2b2245e2d 100644
--- 
a/src/main/java/org/apache/commons/collections4/multiset/AbstractMapMultiSet.java
+++ 
b/src/main/java/org/apache/commons/collections4/multiset/AbstractMapMultiSet.java
@@ -280,8 +280,8 @@ public abstract class AbstractMapMultiSet<E> extends 
AbstractMultiSet<E> {
     /** The map to use to store the data */
     private transient Map<E, MutableInteger> map;
 
-    /** The current total size of the multiset */
-    private transient int size;
+    /** The current total size of the multiset; kept exact past {@link 
Integer#MAX_VALUE}, {@link #size()} saturates */
+    private transient long size;
 
     /** The modification count for fail fast iterators */
     private transient int modCount;
@@ -326,11 +326,13 @@ public abstract class AbstractMapMultiSet<E> extends 
AbstractMultiSet<E> {
 
         if (occurrences > 0) {
             modCount++;
-            size += occurrences;
             if (mut == null) {
                 map.put(object, new MutableInteger(occurrences));
+                size += occurrences;
             } else {
-                mut.value += occurrences;
+                final int applied = Math.min(occurrences, Integer.MAX_VALUE - 
mut.value);
+                mut.value += applied;
+                size += applied;
             }
         }
         return oldCount;
@@ -523,13 +525,14 @@ public abstract class AbstractMapMultiSet<E> extends 
AbstractMultiSet<E> {
     }
 
     /**
-     * Returns the number of elements in this multiset.
+     * Returns the number of elements in this multiset, or {@code 
Integer.MAX_VALUE}
+     * if the multiset contains more than {@code Integer.MAX_VALUE} elements.
      *
      * @return current size of the multiset
      */
     @Override
     public int size() {
-        return size;
+        return (int) Math.min(size, Integer.MAX_VALUE);
     }
 
     /**
diff --git a/src/test/java/org/apache/commons/collections4/bag/HashBagTest.java 
b/src/test/java/org/apache/commons/collections4/bag/HashBagTest.java
index 31837664c..b8671e98a 100644
--- a/src/test/java/org/apache/commons/collections4/bag/HashBagTest.java
+++ b/src/test/java/org/apache/commons/collections4/bag/HashBagTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.collections4.bag;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.io.InvalidObjectException;
@@ -57,6 +58,25 @@ public class HashBagTest<T> extends AbstractBagTest<T> {
         }
     }
 
+    @Test
+    void testAddClampsCountAndSizeToIntegerMaxValue() {
+        final HashBag<String> bag = new HashBag<>();
+        bag.add("X", Integer.MAX_VALUE - 1);
+        bag.add("Y", Integer.MAX_VALUE - 1);
+        assertEquals(Integer.MAX_VALUE, bag.size());
+        bag.add("X", 10);
+        assertEquals(Integer.MAX_VALUE, bag.getCount("X"));
+        assertEquals(Integer.MAX_VALUE, bag.size());
+        assertEquals(2, bag.uniqueSet().size());
+        // true size is 2 * Integer.MAX_VALUE - 11 after this, so size() must 
stay saturated
+        bag.remove("X", 10);
+        assertEquals(Integer.MAX_VALUE - 10, bag.getCount("X"));
+        assertEquals(Integer.MAX_VALUE, bag.size());
+        // size() only drops below Integer.MAX_VALUE once the true size does
+        bag.remove("Y");
+        assertEquals(Integer.MAX_VALUE - 10, bag.size());
+    }
+
 //    void testCreate() throws Exception {
 //        Bag<T> bag = makeObject();
 //        writeExternalFormToDisk((java.io.Serializable) bag, 
"src/test/resources/data/test/HashBag.emptyCollection.version4.obj");
diff --git 
a/src/test/java/org/apache/commons/collections4/multiset/HashMultiSetTest.java 
b/src/test/java/org/apache/commons/collections4/multiset/HashMultiSetTest.java
index 139d95c04..da57c20f8 100644
--- 
a/src/test/java/org/apache/commons/collections4/multiset/HashMultiSetTest.java
+++ 
b/src/test/java/org/apache/commons/collections4/multiset/HashMultiSetTest.java
@@ -68,6 +68,24 @@ public class HashMultiSetTest<T> extends 
AbstractMultiSetTest<T> {
         }
     }
 
+    @Test
+    void testAddClampsCountAndSizeToIntegerMaxValue() {
+        final HashMultiSet<String> set = new HashMultiSet<>();
+        set.add("X", Integer.MAX_VALUE);
+        set.add("X", 1);
+        assertEquals(Integer.MAX_VALUE, set.getCount("X"));
+        assertEquals(Integer.MAX_VALUE, set.size());
+        set.add("Y", Integer.MAX_VALUE);
+        assertEquals(Integer.MAX_VALUE, set.size());
+        // true size is 2 * Integer.MAX_VALUE - 2 after this, so size() must 
stay saturated
+        set.remove("X", 2);
+        assertEquals(Integer.MAX_VALUE - 2, set.getCount("X"));
+        assertEquals(Integer.MAX_VALUE, set.size());
+        // size() only drops below Integer.MAX_VALUE once the true size does
+        set.remove("Y", Integer.MAX_VALUE);
+        assertEquals(Integer.MAX_VALUE - 2, set.size());
+    }
+
 //    void testCreate() throws Exception {
 //        MultiSet<T> multiset = makeObject();
 //        writeExternalFormToDisk((java.io.Serializable) multiset, 
"src/test/resources/data/test/HashMultiSet.emptyCollection.version4.1.obj");

Reply via email to