You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@commons.apache.org by "Ruiqi Dong (Jira)" <ji...@apache.org> on 2025/06/21 12:50:00 UTC

[jira] [Created] (CLI-341) [Apache Commons CLI] HelpFormatter infinite loop when width=0

Ruiqi Dong created CLI-341:
------------------------------

             Summary: [Apache Commons CLI] HelpFormatter infinite loop when width=0
                 Key: CLI-341
                 URL: https://issues.apache.org/jira/browse/CLI-341
             Project: Commons CLI
          Issue Type: Bug
          Components: Help formatter
    Affects Versions: 1.10.0
            Reporter: Ruiqi Dong
             Fix For: 1.10.0
         Attachments: HelpFormatter.java, HelpFormatterTest.java

I am writing to report an infinite loop bug in the HelpFormatter class when the width parameter is set to 0. Despite existing protective code meant to prevent this issue, the infinite loop still occurs under certain conditions.
 
*Problem Description:*
When calling HelpFormatter.printOptions() or any method that uses appendWrappedText() with width=0, the application enters an infinite loop, eventually causing heap space exhaustion.
 
 
*Test Cases:*
public class HelpFormatterTest {
@Test
public void {*}shouldHandleZeroWidthGracefully{*}() {
HelpFormatter formatter = new HelpFormatter();
Options options = new Options();
options.addOption("h", "help", false, "Show help");
StringWriter out = new StringWriter();
PrintWriter pw = new PrintWriter(out);
_// execute - this will trigger an infinite loop, but will be interrupted by @Timeout annotation_
try {
formatter.printOptions(pw, 0, options, 1, 3);
_// if we get here, it means there was no infinite loop (expected behavior after fix)_
String result = out.toString();
assertNotNull(result, "Result should not be null after fix");
} catch (Throwable {_}t{_}) {
_// before the fix, this would catch the timeout exception_
fail("printOptions with width=0 should not cause infinite loop or throw exception");
}
}
@Test
public void {*}demonstrateInfiniteLoopMechanism{*}() {
HelpFormatter formatter = new HelpFormatter();
_// directly test findWrapPos method to demonstrate the root cause_
String text = "Hello World";
int pos = formatter.findWrapPos(text, 0, 0);
_// verify: when width=0, findWrapPos returns 0 (for non-empty text)_
assertEquals(0, pos, "findWrapPos with width=0 returns 0, causing infinite loop");
_// this means the text will never be consumed, because substring(0) returns the entire string_
}
}
 
 
*Test Results:*
[{*}INFO{*}] Running org.apache.commons.cli.{*}HelpFormatterTest{*}
[{*}ERROR{*}] {*}Tests run: 2{*}, {*}Failures: 1{*}, Errors: 0, Skipped: 0, Time elapsed: 25.60 s *<<< FAILURE!* -- in org.apache.commons.cli.{*}HelpFormatterTest{*}
[{*}ERROR{*}] org.apache.commons.cli.HelpFormatterTest.shouldHandleZeroWidthGracefully -- Time elapsed: 25.60 s <<< FAILURE!
org.opentest4j.AssertionFailedError: printOptions with width=0 should not cause infinite loop or throw exception
at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:38)
at org.junit.jupiter.api.Assertions.fail(Assertions.java:138)
at org.apache.commons.cli.HelpFormatterTest.shouldHandleZeroWidthGracefully(HelpFormatterTest.java:54)
at java.lang.reflect.Method.invoke(Method.java:498)
at java.util.ArrayList.forEach(ArrayList.java:1259)
at java.util.ArrayList.forEach(ArrayList.java:1259)
 
 
*Root Cause Analysis:*
I've identified the exact mechanism causing the infinite loop:
1. `findWrapPos(text, 0, 0)` returns 0 for non-empty text (confirmed by my test)
2. In `appendWrappedText()` at line 504: `pos = findWrapPos(render, width, 0);`
3. When pos=0, line 526: `appendable.append(rtrim(render.substring(0, pos)))` appends empty string
4. Line 509 adds a newline
5. Lines 510-512: The protective code sets `nextLineTabStopPos = 1` when `nextLineTabStopPos >= width`
6. However, line 517: `render = padding + render.substring(pos).trim();` 
   - Since pos=0, `render.substring(0)` returns the entire string
   - The text is never consumed, so the loop continues infinitely
 
The protective code (lines 510-513) doesn't help because:
- It only adjusts `nextLineTabStopPos` 
- It doesn't address that `pos` remains 0, causing text to never be consumed
- Each iteration adds a newline and creates new String objects, eventually causing heap exhaustion
 
 
 
*Current Code (HelpFormatter.java, lines 501-528):*
<A extends Appendable> A appendWrappedText(final A appendable, final int width, final int nextLineTabStop, final String text) throws IOException {
        String render = text;
        int nextLineTabStopPos = nextLineTabStop;
        int pos = findWrapPos(render, width, 0);
        if (pos == -1) {
            appendable.append(rtrim(render));
            return appendable;
        }
        appendable.append(rtrim(render.substring(0, pos))).append(getNewLine());
        if (nextLineTabStopPos >= width) {
            // stops infinite loop happening
            nextLineTabStopPos = 1;
        }
        // all following lines must be padded with nextLineTabStop space characters
        final String padding = createPadding(nextLineTabStopPos);
        while (true) {
            render = padding + render.substring(pos).trim();
            pos = findWrapPos(render, width, 0);
            if (pos == -1) {
                appendable.append(render);
                return appendable;
            }
            if (render.length() > width && pos == nextLineTabStopPos - 1) {
                pos = width;
            }
            appendable.append(rtrim(render.substring(0, pos))).append(getNewLine());
        }
    }
 
 
*Why the Existing Protection Fails:*
The comment "stops infinite loop happening" suggests awareness of this issue, but the fix only addresses `nextLineTabStopPos`, not the core problem that `findWrapPos` returns 0 when width=0, preventing text consumption.
 
 
*Proposed Solutions:*
1. Add width validation (recommended):
<A extends Appendable> A appendWrappedText(final A appendable, final int width, 
                                           final int nextLineTabStop, final String text) throws IOException {
    if (width <= 0) {
        throw new IllegalArgumentException("Width must be positive, was: " + width);
    }
    // existing code...
}
 
2. Handle width=0 as special case:
if (width <= 0) {
    appendable.append(text);
    return appendable;
}
 
3. Ensure minimum effective width:
int effectiveWidth = Math.max(width, 1);
int pos = findWrapPos(render, effectiveWidth, 0);
 
*Impact:*
- Any application passing width=0 to HelpFormatter will experience heap exhaustion
- Current protective code is insufficient for the width=0 edge case



--
This message was sent by Atlassian Jira
(v8.20.10#820010)