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 6cc1adac1 validate order and uniqueness invariants in decorator 
readObject (#684)
6cc1adac1 is described below

commit 6cc1adac1596cd9324aab53d8f31eb5c2d610d8d
Author: Dexter.k <[email protected]>
AuthorDate: Fri Jun 19 12:35:31 2026 +0000

    validate order and uniqueness invariants in decorator readObject (#684)
    
    ListOrderedMap, ListOrderedSet and SetUniqueList restore the order/dedup 
structure and the backing collection from the stream as independent fields; 
re-check they agree on read and throw InvalidObjectException for a crafted 
stream that would build a decorator the constructors forbid.
---
 .../commons/collections4/list/SetUniqueList.java   | 18 +++++++++++++++++
 .../commons/collections4/map/ListOrderedMap.java   |  5 +++++
 .../commons/collections4/set/ListOrderedSet.java   | 18 +++++++++++++++++
 .../collections4/list/SetUniqueListTest.java       | 23 ++++++++++++++++++++++
 .../collections4/map/ListOrderedMapTest.java       | 23 ++++++++++++++++++++++
 .../collections4/set/ListOrderedSetTest.java       | 23 ++++++++++++++++++++++
 6 files changed, 110 insertions(+)

diff --git 
a/src/main/java/org/apache/commons/collections4/list/SetUniqueList.java 
b/src/main/java/org/apache/commons/collections4/list/SetUniqueList.java
index 30bba7f9a..286ebe45d 100644
--- a/src/main/java/org/apache/commons/collections4/list/SetUniqueList.java
+++ b/src/main/java/org/apache/commons/collections4/list/SetUniqueList.java
@@ -16,6 +16,9 @@
  */
 package org.apache.commons.collections4.list;
 
+import java.io.IOException;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -427,4 +430,19 @@ public class SetUniqueList<E> extends 
AbstractSerializableListDecorator<E> {
         return ListUtils.unmodifiableList(new SetUniqueList<>(superSubList, 
subSet));
     }
 
+    /**
+     * Deserializes the list and re-checks the no-duplicate invariant the
+     * constructors guarantee.
+     *
+     * @param in  the input stream
+     * @throws IOException if an error occurs while reading from the stream
+     * @throws ClassNotFoundException if a class read from the stream cannot 
be loaded
+     */
+    private void readObject(final ObjectInputStream in) throws IOException, 
ClassNotFoundException {
+        in.defaultReadObject();
+        if (set.size() != size() || !new HashSet<>(decorated()).equals(set)) {
+            throw new InvalidObjectException("Inconsistent SetUniqueList 
deserialized: backing list does not match the uniqueness set");
+        }
+    }
+
 }
diff --git 
a/src/main/java/org/apache/commons/collections4/map/ListOrderedMap.java 
b/src/main/java/org/apache/commons/collections4/map/ListOrderedMap.java
index 248c0fcb5..f8ddedc77 100644
--- a/src/main/java/org/apache/commons/collections4/map/ListOrderedMap.java
+++ b/src/main/java/org/apache/commons/collections4/map/ListOrderedMap.java
@@ -17,6 +17,7 @@
 package org.apache.commons.collections4.map;
 
 import java.io.IOException;
+import java.io.InvalidObjectException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
@@ -25,6 +26,7 @@ import java.util.AbstractSet;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
@@ -687,6 +689,9 @@ public class ListOrderedMap<K, V>
     private void readObject(final ObjectInputStream in) throws IOException, 
ClassNotFoundException {
         in.defaultReadObject();
         map = (Map<K, V>) in.readObject(); // (1)
+        if (insertOrder.size() != map.size() || !new 
HashSet<>(insertOrder).equals(map.keySet())) {
+            throw new InvalidObjectException("Inconsistent ListOrderedMap 
deserialized: key order does not match the map keys");
+        }
     }
 
     /**
diff --git 
a/src/main/java/org/apache/commons/collections4/set/ListOrderedSet.java 
b/src/main/java/org/apache/commons/collections4/set/ListOrderedSet.java
index f318a64b9..5a50de4a4 100644
--- a/src/main/java/org/apache/commons/collections4/set/ListOrderedSet.java
+++ b/src/main/java/org/apache/commons/collections4/set/ListOrderedSet.java
@@ -16,6 +16,9 @@
  */
 package org.apache.commons.collections4.set;
 
+import java.io.IOException;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -413,4 +416,19 @@ public class ListOrderedSet<E>
         return setOrder.toString();
     }
 
+    /**
+     * Deserializes the set and re-checks that the iteration order matches the
+     * decorated set, as the constructors guarantee.
+     *
+     * @param in  the input stream
+     * @throws IOException if an error occurs while reading from the stream
+     * @throws ClassNotFoundException if a class read from the stream cannot 
be loaded
+     */
+    private void readObject(final ObjectInputStream in) throws IOException, 
ClassNotFoundException {
+        in.defaultReadObject();
+        if (setOrder.size() != size() || !new 
HashSet<>(setOrder).equals(decorated())) {
+            throw new InvalidObjectException("Inconsistent ListOrderedSet 
deserialized: iteration order does not match the set");
+        }
+    }
+
 }
diff --git 
a/src/test/java/org/apache/commons/collections4/list/SetUniqueListTest.java 
b/src/test/java/org/apache/commons/collections4/list/SetUniqueListTest.java
index 390b66de2..d03ac8a3e 100644
--- a/src/test/java/org/apache/commons/collections4/list/SetUniqueListTest.java
+++ b/src/test/java/org/apache/commons/collections4/list/SetUniqueListTest.java
@@ -22,6 +22,11 @@ import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -101,6 +106,24 @@ public class SetUniqueListTest<E> extends 
AbstractListTest<E> {
         return new SetUniqueList<>(new ArrayList<>(), new HashSet<>());
     }
 
+    @Test
+    void testDeserializeRejectsDuplicateInBackingList() throws Exception {
+        final SetUniqueList<String> list = SetUniqueList.setUniqueList(new 
ArrayList<>());
+        list.add("alpha");
+        list.add("beta");
+        // push a duplicate straight onto the decorated list, bypassing the 
uniqueness set
+        list.decorated().add("alpha");
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try (ObjectOutputStream oos = new ObjectOutputStream(out)) {
+            oos.writeObject(list);
+        }
+        assertThrows(InvalidObjectException.class, () -> {
+            try (ObjectInputStream ois = new ObjectInputStream(new 
ByteArrayInputStream(out.toByteArray()))) {
+                ois.readObject();
+            }
+        });
+    }
+
     @Test
     @SuppressWarnings("unchecked")
     void testAdd() {
diff --git 
a/src/test/java/org/apache/commons/collections4/map/ListOrderedMapTest.java 
b/src/test/java/org/apache/commons/collections4/map/ListOrderedMapTest.java
index b9d504617..9b35f81df 100644
--- a/src/test/java/org/apache/commons/collections4/map/ListOrderedMapTest.java
+++ b/src/test/java/org/apache/commons/collections4/map/ListOrderedMapTest.java
@@ -21,6 +21,11 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -166,6 +171,24 @@ public class ListOrderedMapTest<K, V> extends 
AbstractOrderedMapTest<K, V> {
         return ListOrderedMap.listOrderedMap(new HashMap<>());
     }
 
+    @Test
+    void testDeserializeRejectsKeyOrderMismatch() throws Exception {
+        final ListOrderedMap<String, String> map = new ListOrderedMap<>();
+        map.put("one", "1");
+        map.put("two", "2");
+        // drop a key straight from the backing map; the insert-order list 
still names it
+        map.decorated().remove("one");
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try (ObjectOutputStream oos = new ObjectOutputStream(out)) {
+            oos.writeObject(map);
+        }
+        assertThrows(InvalidObjectException.class, () -> {
+            try (ObjectInputStream ois = new ObjectInputStream(new 
ByteArrayInputStream(out.toByteArray()))) {
+                ois.readObject();
+            }
+        });
+    }
+
     @Test
     void testCOLLECTIONS_474_nonNullValues() {
         final Object key1 = new Object();
diff --git 
a/src/test/java/org/apache/commons/collections4/set/ListOrderedSetTest.java 
b/src/test/java/org/apache/commons/collections4/set/ListOrderedSetTest.java
index f7afc09e7..c08b1bb8d 100644
--- a/src/test/java/org/apache/commons/collections4/set/ListOrderedSetTest.java
+++ b/src/test/java/org/apache/commons/collections4/set/ListOrderedSetTest.java
@@ -21,6 +21,11 @@ import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InvalidObjectException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -92,6 +97,24 @@ public class ListOrderedSetTest<E>
         return set;
     }
 
+    @Test
+    void testDeserializeRejectsOrderMismatch() throws Exception {
+        final ListOrderedSet<String> set = ListOrderedSet.listOrderedSet(new 
HashSet<>());
+        set.add("red");
+        set.add("green");
+        // remove an element from the decorated set only; the order list keeps 
naming it
+        set.decorated().remove("red");
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        try (ObjectOutputStream oos = new ObjectOutputStream(out)) {
+            oos.writeObject(set);
+        }
+        assertThrows(InvalidObjectException.class, () -> {
+            try (ObjectInputStream ois = new ObjectInputStream(new 
ByteArrayInputStream(out.toByteArray()))) {
+                ois.readObject();
+            }
+        });
+    }
+
     @Test
     void testDecorator() {
         assertThrows(NullPointerException.class, () -> 
ListOrderedSet.listOrderedSet((List<E>) null));

Reply via email to