You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by pk...@apache.org on 2026/05/17 21:27:37 UTC
(commons-lang) branch fix/pattern-length created (now 9c4422421)
This is an automated email from the ASF dual-hosted git repository.
ppkarwasz pushed a change to branch fix/pattern-length
in repository https://gitbox.apache.org/repos/asf/commons-lang.git
at 9c4422421 Fix `WordUtils.wrap` for `wrapOn` matches different from one character
This branch includes the following new commits:
new 9c4422421 Fix `WordUtils.wrap` for `wrapOn` matches different from one character
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(commons-lang) 01/01: Fix `WordUtils.wrap` for `wrapOn` matches different from one character
Posted by pk...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
ppkarwasz pushed a commit to branch fix/pattern-length
in repository https://gitbox.apache.org/repos/asf/commons-lang.git
commit 9c4422421b86fc72226f50d807cc392204a0d1a2
Author: Piotr P. Karwasz <pk...@apache.org>
AuthorDate: Sun May 17 23:19:26 2026 +0200
Fix `WordUtils.wrap` for `wrapOn` matches different from one character
`WordUtils.wrap` advanced the offset by a single character past each `wrapOn` match, assuming the separator is always exactly one character long:
- When the `wrapOn` regex matched several characters, the surplus separator characters were left in the wrapped output instead of being consumed,
- When the `wrapOn` regex matched zero characters, one additional character was consumed.
Now it tracks the end offset of each match and resume from there, so that separators of any length, including zero-width matches, are skipped correctly.
---
src/changes/changes.xml | 1 +
.../org/apache/commons/lang3/text/WordUtils.java | 21 +++---
.../apache/commons/lang3/text/WordUtilsTest.java | 82 ++++++++++++++--------
3 files changed, 65 insertions(+), 39 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 997c67b80..e0a55d869 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -152,6 +152,7 @@ java.lang.NullPointerException: Cannot invoke
<action type="fix" dev="ggregory" due-to="Omkhar Arasaratnam, Gary Gregory">WordUtils.wrap() zero-width wrapOn regex match causes infinite loop (#1649).WordUtils.wrap() zero-width wrapOn regex match causes infinite loop (#1649).</action>
<action type="fix" dev="ggregory" due-to="Omkhar Arasaratnam, Gary Gregory">HashCodeBuilder.append(Object) StackOverflowError on mutually-referential objects (#1650).</action>
<action type="fix" dev="ggregory" due-to="Omkhar Arasaratnam, Gary Gregory">AtomicSafeInitializer.get() busy-spin without yield burns CPU during slow initialization (#1651).</action>
+ <action type="fix" dev="pkarwasz" due-to="Piotr Karwasz">WordUtils.wrap() leaves separator characters in the output when the wrapOn regex match is longer than one character (#1655).</action>
<!-- ADD -->
<action type="add" dev="ggregory" due-to="Gary Gregory">Add JavaVersion.JAVA_27.</action>
<action type="add" dev="ggregory" due-to="Gary Gregory">Add SystemUtils.IS_JAVA_27.</action>
diff --git a/src/main/java/org/apache/commons/lang3/text/WordUtils.java b/src/main/java/org/apache/commons/lang3/text/WordUtils.java
index ae59a34e3..feed32d15 100644
--- a/src/main/java/org/apache/commons/lang3/text/WordUtils.java
+++ b/src/main/java/org/apache/commons/lang3/text/WordUtils.java
@@ -648,15 +648,17 @@ public static String wrap(final String str, int wrapLength, String newLineStr, f
while (offset < inputLineLength) {
int spaceToWrapAt = -1;
+ int endOfWrapAt = -1;
Matcher matcher = patternToWrapOn.matcher(
str.substring(offset, Math.min((int) Math.min(Integer.MAX_VALUE, offset + wrapLength + 1L), inputLineLength)));
if (matcher.find()) {
- if (matcher.start() == 0) {
- // If the match is zero-width, advance by at least 1 to avoid infinite loop.
- offset += matcher.end() > 0 ? matcher.end() : 1;
+ spaceToWrapAt = matcher.start() + offset;
+ endOfWrapAt = matcher.end() + offset;
+ // Skip leading match, if it is not zero-width
+ if (spaceToWrapAt == offset && endOfWrapAt != offset) {
+ offset = endOfWrapAt;
continue;
}
- spaceToWrapAt = matcher.start() + offset;
}
// only last line without leading spaces is left
if (inputLineLength - offset <= wrapLength) {
@@ -664,13 +666,13 @@ public static String wrap(final String str, int wrapLength, String newLineStr, f
}
while (matcher.find()) {
spaceToWrapAt = matcher.start() + offset;
+ endOfWrapAt = matcher.end() + offset;
}
- if (spaceToWrapAt >= offset) {
+ if (endOfWrapAt > offset) {
// normal case
wrappedLine.append(str, offset, spaceToWrapAt);
wrappedLine.append(newLineStr);
- offset = spaceToWrapAt + 1;
-
+ offset = endOfWrapAt;
} else // really long word or URL
if (wrapLongWords) {
// wrap really long word one line at a time
@@ -680,14 +682,17 @@ public static String wrap(final String str, int wrapLength, String newLineStr, f
} else {
// do not wrap really long word, just extend beyond limit
matcher = patternToWrapOn.matcher(str.substring(offset + wrapLength));
+ spaceToWrapAt = -1;
if (matcher.find()) {
spaceToWrapAt = matcher.start() + offset + wrapLength;
+ endOfWrapAt = matcher.end() + offset + wrapLength;
}
if (spaceToWrapAt >= 0) {
wrappedLine.append(str, offset, spaceToWrapAt);
wrappedLine.append(newLineStr);
- offset = spaceToWrapAt + 1;
+ // at least offset + wrapLength >= offset + 1
+ offset = endOfWrapAt;
} else {
wrappedLine.append(str, offset, str.length());
offset = inputLineLength;
diff --git a/src/test/java/org/apache/commons/lang3/text/WordUtilsTest.java b/src/test/java/org/apache/commons/lang3/text/WordUtilsTest.java
index e14b14cbf..479b62640 100644
--- a/src/test/java/org/apache/commons/lang3/text/WordUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/text/WordUtilsTest.java
@@ -20,15 +20,19 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
-import java.time.Duration;
+import java.util.stream.Stream;
import org.apache.commons.lang3.AbstractLangTest;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
/**
* Tests for WordUtils class.
@@ -36,6 +40,42 @@
@Deprecated
class WordUtilsTest extends AbstractLangTest {
+ static Stream<Arguments> testWrapStringIntStringBooleanString() {
+ return Stream.of(
+ // null passthrough
+ arguments(null, -1, false, "/", null),
+ // no changes test
+ arguments("flammable/inflammable", 30, false, "/", "flammable/inflammable"),
+ // wrap on / and small width
+ arguments("flammable/inflammable", 2, false, "/", "flammable\ninflammable"),
+ // wrap long words on / 1
+ arguments("flammable/inflammable", 9, true, "/", "flammable\ninflammab\nle"),
+ // wrap long words on / 2
+ arguments("flammable/inflammable", 15, true, "/", "flammable\ninflammable"),
+ // wrap long words on / 3
+ arguments("flammableinflammable", 15, true, "/", "flammableinflam\nmable"),
+ // default values
+ arguments("a/a/a/a", -1, false, "/", "a\na\na\na"),
+ arguments("a a a a", 1, false, null, "a\na\na\na"),
+ // strip leading / keep trailing
+ arguments("///abc///def///ghi", 3, false, "/", "abc\ndef\nghi"),
+ arguments("///abc///def///ghi", 4, false, "/", "abc/\ndef/\nghi"),
+ arguments("///abc///def///ghi", 5, false, "/", "abc//\ndef//\nghi"),
+ // keep only two trailing, wrap on third
+ arguments("///abc///def///ghi", 6, false, "/", "abc//\ndef//\nghi"),
+ // zero-width regex match must advance to avoid an infinite loop
+ arguments("abcabc", 3, false, "(?=a)", "abc\nabc"),
+ arguments("abcdefabcdef", 4, false, "(?=a)", "abcdef\nabcdef"),
+ arguments("abcdefabcdef", 4, true, "(?=a)", "abcd\nef\nabcd\nef"),
+ // width two regex
+ arguments("abc\\/abc", 3, false, "\\\\/", "abc\nabc"),
+ arguments("abcdef\\/abcdef", 4, false, "\\\\/", "abcdef\nabcdef"),
+ arguments("abcdef\\/abcdef", 4, true, "\\\\/", "abcd\nef\nabcd\nef"),
+ // variable-width regex
+ arguments(".abc.-def.--ghi", 5, false, "[.]-*", "abc\ndef\nghi")
+ );
+ }
+
@Test
void testCapitalize_String() {
assertNull(WordUtils.capitalize(null));
@@ -406,34 +446,14 @@ void testWrap_StringIntStringBoolean() {
assertEquals(expected, WordUtils.wrap(input, 20, "\n", true));
}
- @Test
- void testWrap_StringIntStringBooleanString() {
-
- //no changes test
- String input = "flammable/inflammable";
- String expected = "flammable/inflammable";
- assertEquals(expected, WordUtils.wrap(input, 30, "\n", false, "/"));
-
- // wrap on / and small width
- expected = "flammable\ninflammable";
- assertEquals(expected, WordUtils.wrap(input, 2, "\n", false, "/"));
-
- // wrap long words on / 1
- expected = "flammable\ninflammab\nle";
- assertEquals(expected, WordUtils.wrap(input, 9, "\n", true, "/"));
-
- // wrap long words on / 2
- expected = "flammable\ninflammable";
- assertEquals(expected, WordUtils.wrap(input, 15, "\n", true, "/"));
-
- // wrap long words on / 3
- input = "flammableinflammable";
- expected = "flammableinflam\nmable";
- assertEquals(expected, WordUtils.wrap(input, 15, "\n", true, "/"));
- }
-
- @Test
- void testZeroWidthWrapOnRegex() {
- assertTimeout(Duration.ofSeconds(2), () -> assertNotNull(WordUtils.wrap("abcdef", 3, "\n", false, "(?=a)")));
+ @ParameterizedTest
+ @MethodSource
+ @Timeout(2)
+ void testWrapStringIntStringBooleanString(final String str, final int wrapLength, final boolean wrapLongWords, final String wrapOn, final String expected) {
+ assertEquals(expected, WordUtils.wrap(str, wrapLength, "\n", wrapLongWords, wrapOn));
+ final String sep = System.lineSeparator();
+ if (!sep.equals("\n")) {
+ assertEquals(expected != null ? expected.replace("\n", sep) : null, WordUtils.wrap(str, wrapLength, null, wrapLongWords, wrapOn));
+ }
}
}