LOG4J2-2948 Replace HashSet with IdentityHashMap in ParameterFormatter to detect cycles. (#471)
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java
index 0199e67..3609d8e 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java
@@ -367,8 +367,8 @@
             sb.append("  <Entry key=\"")
                     .append(data.getKeyAt(i))
                     .append("\">");
-            int size = sb.length();
-            ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
+            final int size = sb.length();
+            ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb);
             StringBuilders.escapeXml(sb, size);
             sb.append("</Entry>\n");
         }
@@ -418,7 +418,7 @@
                 sb.append(' ');
             }
             sb.append(data.getKeyAt(i)).append(Chars.EQ).append(Chars.DQUOTE);
-            ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
+            ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb);
             sb.append(Chars.DQUOTE);
         }
     }
@@ -445,7 +445,7 @@
             if (quoted) {
                 sb.append(Chars.DQUOTE);
             }
-            ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
+            ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb);
             if (quoted) {
                 sb.append(Chars.DQUOTE);
             }
@@ -474,7 +474,7 @@
     }
 
     @Override
-    public void formatTo(String[] formats, StringBuilder buffer) {
+    public void formatTo(final String[] formats, final StringBuilder buffer) {
         format(getFormat(formats), buffer);
     }
 
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterFormatter.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterFormatter.java
index c01e686..55982e0 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterFormatter.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterFormatter.java
@@ -20,8 +20,9 @@
 import java.time.format.DateTimeFormatter;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Date;
-import java.util.HashSet;
+import java.util.IdentityHashMap;
 import java.util.Map;
 import java.util.Set;
 
@@ -61,7 +62,9 @@
     private static final char DELIM_STOP = '}';
     private static final char ESCAPE_CHAR = '\\';
     private static final DateTimeFormatter FORMATTER =
-            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").withZone(ZoneId.systemDefault());
+            DateTimeFormatter
+                    .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
+                    .withZone(ZoneId.systemDefault());
 
     private ParameterFormatter() {
     }
@@ -187,7 +190,7 @@
         for (int i = 0; i < argCount; i++) {
             buffer.append(messagePattern, previous, indices[i]);
             previous = indices[i] + 2;
-            recursiveDeepToString(arguments[i], buffer, null);
+            recursiveDeepToString(arguments[i], buffer);
         }
         buffer.append(messagePattern, previous, messagePattern.length());
     }
@@ -212,7 +215,7 @@
         for (int i = 0; i < argCount; i++) {
             buffer.append(messagePattern, previous, indices[i]);
             previous = indices[i] + 2;
-            recursiveDeepToString(arguments[i], buffer, null);
+            recursiveDeepToString(arguments[i], buffer);
         }
         buffer.append(messagePattern, previous, patternLength);
     }
@@ -364,7 +367,7 @@
     private static void writeArgOrDelimPair(final Object[] arguments, final int argCount, final int currentArgument,
             final StringBuilder buffer) {
         if (currentArgument < argCount) {
-            recursiveDeepToString(arguments[currentArgument], buffer, null);
+            recursiveDeepToString(arguments[currentArgument], buffer);
         } else {
             writeDelimPair(buffer);
         }
@@ -421,35 +424,54 @@
             return Byte.toString((Byte) o);
         }
         final StringBuilder str = new StringBuilder();
-        recursiveDeepToString(o, str, null);
+        recursiveDeepToString(o, str);
         return str.toString();
     }
 
     /**
-     * This method performs a deep toString of the given Object.
-     * Primitive arrays are converted using their respective Arrays.toString methods while
-     * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
-     * contain themselves.
+     * This method performs a deep {@code toString()} of the given {@code Object}.
      * <p>
-     * dejaVu is used in case of those container types to prevent an endless recursion.
-     * </p>
+     * Primitive arrays are converted using their respective {@code Arrays.toString()} methods, while
+     * special handling is implemented for <i>container types</i>, i.e. {@code Object[]}, {@code Map} and {@code Collection},
+     * because those could contain themselves.
      * <p>
-     * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
-     * behavior.
-     * They only check if the container is directly contained in itself, but not if a contained container contains the
-     * original one. Because of that, Arrays.toString(Object[]) isn't safe either.
-     * Confusing? Just read the last paragraph again and check the respective toString() implementation.
-     * </p>
+     * It should be noted that neither {@code AbstractMap.toString()} nor {@code AbstractCollection.toString()} implement such a behavior.
+     * They only check if the container is directly contained in itself, but not if a contained container contains the original one.
+     * Because of that, {@code Arrays.toString(Object[])} isn't safe either.
+     * Confusing? Just read the last paragraph again and check the respective {@code toString()} implementation.
      * <p>
-     * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o)
-     * would produce a relatively hard-to-debug StackOverflowError.
-     * </p>
+     * This means, in effect, that logging would produce a usable output even if an ordinary {@code System.out.println(o)}
+     * would produce a relatively hard-to-debug {@code StackOverflowError}.
      *
-     * @param o      the Object to convert into a String
-     * @param str    the StringBuilder that o will be appended to
-     * @param dejaVu a list of container identities that were already used.
+     * @param o      the {@code Object} to convert into a {@code String}
+     * @param str    the {@code StringBuilder} that {@code o} will be appended to
      */
-    static void recursiveDeepToString(final Object o, final StringBuilder str, final Set<String> dejaVu) {
+    static void recursiveDeepToString(final Object o, final StringBuilder str) {
+        recursiveDeepToString(o, str, null);
+    }
+
+    /**
+     * This method performs a deep {@code toString()} of the given {@code Object}.
+     * <p>
+     * Primitive arrays are converted using their respective {@code Arrays.toString()} methods, while
+     * special handling is implemented for <i>container types</i>, i.e. {@code Object[]}, {@code Map} and {@code Collection},
+     * because those could contain themselves.
+     * <p>
+     * {@code dejaVu} is used in case of those container types to prevent an endless recursion.
+     * <p>
+     * It should be noted that neither {@code AbstractMap.toString()} nor {@code AbstractCollection.toString()} implement such a behavior.
+     * They only check if the container is directly contained in itself, but not if a contained container contains the original one.
+     * Because of that, {@code Arrays.toString(Object[])} isn't safe either.
+     * Confusing? Just read the last paragraph again and check the respective {@code toString()} implementation.
+     * <p>
+     * This means, in effect, that logging would produce a usable output even if an ordinary {@code System.out.println(o)}
+     * would produce a relatively hard-to-debug {@code StackOverflowError}.
+     *
+     * @param o      the {@code Object} to convert into a {@code String}
+     * @param str    the {@code StringBuilder} that {@code o} will be appended to
+     * @param dejaVu a set of container objects directly or transitively containing {@code o}
+     */
+    private static void recursiveDeepToString(final Object o, final StringBuilder str, final Set<Object> dejaVu) {
         if (appendSpecialTypes(o, str)) {
             return;
         }
@@ -479,8 +501,10 @@
         return o.getClass().isArray() || o instanceof Map || o instanceof Collection;
     }
 
-    private static void appendPotentiallyRecursiveValue(final Object o, final StringBuilder str,
-            final Set<String> dejaVu) {
+    private static void appendPotentiallyRecursiveValue(
+            final Object o,
+            final StringBuilder str,
+            final Set<Object> dejaVu) {
         final Class<?> oClass = o.getClass();
         if (oClass.isArray()) {
             appendArray(o, str, dejaVu, oClass);
@@ -488,10 +512,15 @@
             appendMap(o, str, dejaVu);
         } else if (o instanceof Collection) {
             appendCollection(o, str, dejaVu);
+        } else {
+            throw new IllegalArgumentException("was expecting a container, found " + oClass);
         }
     }
 
-    private static void appendArray(final Object o, final StringBuilder str, Set<String> dejaVu,
+    private static void appendArray(
+            final Object o,
+            final StringBuilder str,
+            final Set<Object> dejaVu,
             final Class<?> oClass) {
         if (oClass == byte[].class) {
             str.append(Arrays.toString((byte[]) o));
@@ -510,15 +539,13 @@
         } else if (oClass == char[].class) {
             str.append(Arrays.toString((char[]) o));
         } else {
-            if (dejaVu == null) {
-                dejaVu = new HashSet<>();
-            }
             // special handling of container Object[]
-            final String id = identityToString(o);
-            if (dejaVu.contains(id)) {
+            final Set<Object> effectiveDejaVu = getOrCreateDejaVu(dejaVu);
+            final boolean seen = !effectiveDejaVu.add(o);
+            if (seen) {
+                final String id = identityToString(o);
                 str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
             } else {
-                dejaVu.add(id);
                 final Object[] oArray = (Object[]) o;
                 str.append('[');
                 boolean first = true;
@@ -528,24 +555,26 @@
                     } else {
                         str.append(", ");
                     }
-                    recursiveDeepToString(current, str, new HashSet<>(dejaVu));
+                    recursiveDeepToString(current, str, cloneDejaVu(effectiveDejaVu));
                 }
                 str.append(']');
             }
-            //str.append(Arrays.deepToString((Object[]) o));
         }
     }
 
-    private static void appendMap(final Object o, final StringBuilder str, Set<String> dejaVu) {
-        // special handling of container Map
-        if (dejaVu == null) {
-            dejaVu = new HashSet<>();
-        }
-        final String id = identityToString(o);
-        if (dejaVu.contains(id)) {
+    /**
+     * Specialized handler for {@link Map}s.
+     */
+    private static void appendMap(
+            final Object o,
+            final StringBuilder str,
+            final Set<Object> dejaVu) {
+        final Set<Object> effectiveDejaVu = getOrCreateDejaVu(dejaVu);
+        final boolean seen = !effectiveDejaVu.add(o);
+        if (seen) {
+            final String id = identityToString(o);
             str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
         } else {
-            dejaVu.add(id);
             final Map<?, ?> oMap = (Map<?, ?>) o;
             str.append('{');
             boolean isFirst = true;
@@ -558,24 +587,27 @@
                 }
                 final Object key = current.getKey();
                 final Object value = current.getValue();
-                recursiveDeepToString(key, str, new HashSet<>(dejaVu));
+                recursiveDeepToString(key, str, cloneDejaVu(effectiveDejaVu));
                 str.append('=');
-                recursiveDeepToString(value, str, new HashSet<>(dejaVu));
+                recursiveDeepToString(value, str, cloneDejaVu(effectiveDejaVu));
             }
             str.append('}');
         }
     }
 
-    private static void appendCollection(final Object o, final StringBuilder str, Set<String> dejaVu) {
-        // special handling of container Collection
-        if (dejaVu == null) {
-            dejaVu = new HashSet<>();
-        }
-        final String id = identityToString(o);
-        if (dejaVu.contains(id)) {
+    /**
+     * Specialized handler for {@link Collection}s.
+     */
+    private static void appendCollection(
+            final Object o,
+            final StringBuilder str,
+            final Set<Object> dejaVu) {
+        final Set<Object> effectiveDejaVu = getOrCreateDejaVu(dejaVu);
+        final boolean seen = !effectiveDejaVu.add(o);
+        if (seen) {
+            final String id = identityToString(o);
             str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
         } else {
-            dejaVu.add(id);
             final Collection<?> oCol = (Collection<?>) o;
             str.append('[');
             boolean isFirst = true;
@@ -585,12 +617,28 @@
                 } else {
                     str.append(", ");
                 }
-                recursiveDeepToString(anOCol, str, new HashSet<>(dejaVu));
+                recursiveDeepToString(anOCol, str, cloneDejaVu(effectiveDejaVu));
             }
             str.append(']');
         }
     }
 
+    private static Set<Object> getOrCreateDejaVu(Set<Object> dejaVu) {
+        return dejaVu == null
+                ? createDejaVu()
+                : dejaVu;
+    }
+
+    private static Set<Object> createDejaVu() {
+        return Collections.newSetFromMap(new IdentityHashMap<>());
+    }
+
+    private static Set<Object> cloneDejaVu(Set<Object> dejaVu) {
+        Set<Object> clonedDejaVu = createDejaVu();
+        clonedDejaVu.addAll(dejaVu);
+        return clonedDejaVu;
+    }
+
     private static void tryObjectToString(final Object o, final StringBuilder str) {
         // it's just some other Object, we can only use toString().
         try {
diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/message/ParameterFormatterTest.java b/log4j-api/src/test/java/org/apache/logging/log4j/message/ParameterFormatterTest.java
index 92d4afb..ba793e3 100644
--- a/log4j-api/src/test/java/org/apache/logging/log4j/message/ParameterFormatterTest.java
+++ b/log4j-api/src/test/java/org/apache/logging/log4j/message/ParameterFormatterTest.java
@@ -19,17 +19,18 @@
 import org.junit.jupiter.api.Test;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 /**
- * Tests ParameterFormatter.
+ * Tests {@link ParameterFormatter}.
  */
 public class ParameterFormatterTest {
 
     @Test
-    public void testCountArgumentPlaceholders() throws Exception {
+    public void testCountArgumentPlaceholders() {
         assertEquals(0, ParameterFormatter.countArgumentPlaceholders(""));
         assertEquals(0, ParameterFormatter.countArgumentPlaceholders("aaa"));
         assertEquals(0, ParameterFormatter.countArgumentPlaceholders("\\{}"));
@@ -169,9 +170,10 @@
     }
 
     @Test
-    public void testDeepToString() throws Exception {
+    public void testDeepToString() {
         final List<Object> list = new ArrayList<>();
         list.add(1);
+        // noinspection CollectionAddedToSelf
         list.add(list);
         list.add(2);
         final String actual = ParameterFormatter.deepToString(list);
@@ -180,13 +182,29 @@
     }
 
     @Test
-    public void testIdentityToString() throws Exception {
+    public void testDeepToStringUsingNonRecursiveButConsequentObjects() {
+        final List<Object> list = new ArrayList<>();
+        final Object item = Collections.singletonList(0);
+        list.add(1);
+        list.add(item);
+        list.add(2);
+        list.add(item);
+        list.add(3);
+        final String actual = ParameterFormatter.deepToString(list);
+        final String expected = "[1, [0], 2, [0], 3]";
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void testIdentityToString() {
         final List<Object> list = new ArrayList<>();
         list.add(1);
+        // noinspection CollectionAddedToSelf
         list.add(list);
         list.add(2);
         final String actual = ParameterFormatter.identityToString(list);
         final String expected = list.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(list));
         assertEquals(expected, actual);
     }
+
 }
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index f36f1df..15faa0b 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -31,6 +31,9 @@
          - "remove" - Removed
     -->
     <release version="3.0.0" date="2021-MM-DD" description="GA Release 3.0.0">
+      <action issue="LOG4J2-2948" dev="vy" type="fix">
+        Replace HashSet with IdentityHashMap in ParameterFormatter to detect cycles.
+      </action>
       <action issue="LOG4J2-2624" dev="mattsicker" type="fix" due-to="Tim Perry">
         Allow auto-shutdown of log4j in log4j-web to be turned off and provide a
         ServletContextListener "Log4jShutdownOnContextDestroyedListener" to stop log4j.