From 972aa7b29a7cb7306b9e0737bac86fa42d6218ac Mon Sep 17 00:00:00 2001 From: "Capt. Cutlass" <5120290+ParanoidUser@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:31:23 -0400 Subject: [PATCH] fix: default ttl in recursive replacement (#1297) https://issues.apache.org/jira/browse/LANG-1753 --- .../org/apache/commons/lang3/StringUtils.java | 26 +++++----- .../apache/commons/lang3/StringUtilsTest.java | 48 ++++++++++++++++++- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/StringUtils.java b/src/main/java/org/apache/commons/lang3/StringUtils.java index b9403c79347..70c26ee315b 100644 --- a/src/main/java/org/apache/commons/lang3/StringUtils.java +++ b/src/main/java/org/apache/commons/lang3/StringUtils.java @@ -21,12 +21,10 @@ import java.text.Normalizer; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Objects; -import java.util.Set; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -181,6 +179,11 @@ public class StringUtils { */ private static final int PAD_LIMIT = 8192; + /** + * The default maximum depth at which recursive replacement will continue until no further search replacements are possible. + */ + private static final int DEFAULT_TTL = 5; + /** * Pattern used in {@link #stripAccents(String)}. */ @@ -6450,20 +6453,14 @@ private static String replaceEach( // mchyzer Performance note: This creates very few new objects (one major goal) // let me know if there are performance requests, we can create a harness to measure + if (isEmpty(text) || ArrayUtils.isEmpty(searchList) || ArrayUtils.isEmpty(replacementList)) { + return text; + } // if recursing, this shouldn't be less than 0 if (timeToLive < 0) { - final Set searchSet = new HashSet<>(Arrays.asList(searchList)); - final Set replacementSet = new HashSet<>(Arrays.asList(replacementList)); - searchSet.retainAll(replacementSet); - if (!searchSet.isEmpty()) { - throw new IllegalStateException("Aborting to protect against StackOverflowError - " + - "output of one loop is the input of another"); - } - } - - if (isEmpty(text) || ArrayUtils.isEmpty(searchList) || ArrayUtils.isEmpty(replacementList) || ArrayUtils.isNotEmpty(searchList) && timeToLive == -1) { - return text; + throw new IllegalStateException("Aborting to protect against StackOverflowError - " + + "output of one loop is the input of another"); } final int searchLength = searchList.length; @@ -6611,7 +6608,8 @@ private static String replaceEach( * @since 2.4 */ public static String replaceEachRepeatedly(final String text, final String[] searchList, final String[] replacementList) { - return replaceEach(text, searchList, replacementList, true, ArrayUtils.getLength(searchList)); + int timeToLive = Math.max(ArrayUtils.getLength(searchList), DEFAULT_TTL); + return replaceEach(text, searchList, replacementList, true, timeToLive); } /** diff --git a/src/test/java/org/apache/commons/lang3/StringUtilsTest.java b/src/test/java/org/apache/commons/lang3/StringUtilsTest.java index e1102f74e64..8f15669b329 100644 --- a/src/test/java/org/apache/commons/lang3/StringUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/StringUtilsTest.java @@ -1990,7 +1990,53 @@ public void testReplace_StringStringArrayStringArrayBoolean() { assertEquals("aba", StringUtils.replaceEachRepeatedly("aba", new String[]{null}, new String[]{"a"})); assertEquals("wcte", StringUtils.replaceEachRepeatedly("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"})); assertEquals("tcte", StringUtils.replaceEachRepeatedly("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"})); - assertEquals("blaan", StringUtils.replaceEachRepeatedly("blllaan", new String[]{"llaan"}, new String[]{"laan"}) ); + + // Test recursive replacement - LANG-1528 & LANG-1753 + assertEquals("blaan", StringUtils.replaceEachRepeatedly("blllaan", new String[]{"llaan"}, new String[]{"laan"})); + assertEquals("blaan", StringUtils.replaceEachRepeatedly("bllllaan", new String[]{"llaan"}, new String[]{"laan"})); + + // Test default TTL for smaller search lists. 32 characters reduced to 16, then 8, 4, 2, 1. + assertEquals("a", StringUtils.replaceEachRepeatedly("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + new String[]{"aa"}, new String[]{"a"})); + + // Test default TTL exceeded. 33 characters reduced to 17, then 9, 5, 3, 2 (still found). + assertThrows( + IllegalStateException.class, + () -> StringUtils.replaceEachRepeatedly("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + new String[]{"aa"}, new String[]{"a"}), + "Cannot be resolved within the default time-to-live limit"); + + // Test larger TTL for larger search lists. Replace repeatedly until there are no more possible replacements. + assertEquals("000000000", StringUtils.replaceEachRepeatedly("aA0aA0aA0", + new String[]{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", + "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", + "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", + "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9"}, + new String[]{"b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", + "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", + "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", + "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0"})); + + // Test long infinite cycle: a -> b -> ... -> 9 -> 0 -> a -> b -> ... + assertThrows( + IllegalStateException.class, + () -> StringUtils.replaceEachRepeatedly("a", + new String[]{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", + "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", + "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", + "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0"}, + new String[]{"b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", + "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C", "D", "E", + "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", + "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a"}), + "Should be a circular reference"); + + assertThrows( + IllegalStateException.class, + () -> StringUtils.replaceEachRepeatedly("%{key1}", + new String[] {"%{key1}", "%{key2}", "%{key3}"}, + new String[] {"Key1 %{key2}", "Key2 %{key3}", "Key3 %{key1}"}), + "Should be a circular reference"); assertThrows( IllegalStateException.class,