diff --git a/src/main/java/org/apache/commons/csv/CSVFormat.java b/src/main/java/org/apache/commons/csv/CSVFormat.java index 2e7e12a79..6a686e468 100644 --- a/src/main/java/org/apache/commons/csv/CSVFormat.java +++ b/src/main/java/org/apache/commons/csv/CSVFormat.java @@ -2467,8 +2467,10 @@ private void printWithEscapes(final Reader reader, final Appendable appendable) builder.append((char) c); Arrays.fill(lookAheadBuffer, (char) 0); bufferedReader.peek(lookAheadBuffer); - final String test = builder.toString() + new String(lookAheadBuffer); - final boolean isDelimiterStart = isDelimiter((char) c, test, pos, delimArray, delimLength); + // Match the delimiter against the current character plus the look-ahead buffer only. Rebuilding the test + // string from the whole accumulated builder made this loop O(n^2) for values without escapable characters. + final String test = String.valueOf((char) c) + new String(lookAheadBuffer); + final boolean isDelimiterStart = isDelimiter((char) c, test, 0, delimArray, delimLength); final boolean isCr = c == Constants.CR; final boolean isLf = c == Constants.LF; // A leading comment marker would be read back as a comment, so escape it. diff --git a/src/test/java/org/apache/commons/csv/CSVFormatTest.java b/src/test/java/org/apache/commons/csv/CSVFormatTest.java index 999527cdb..feebc408c 100644 --- a/src/test/java/org/apache/commons/csv/CSVFormatTest.java +++ b/src/test/java/org/apache/commons/csv/CSVFormatTest.java @@ -974,6 +974,22 @@ void testPrintWithEscapesEndWithoutCRLF() throws IOException { assertEquals("x?,y?,x", out.toString()); } + @Test + void testPrintWithEscapesReaderLargeValueIsLinear() { + // A Reader value with no escapable characters used to run in O(n^2): the delimiter look-ahead rebuilt a + // String from the whole accumulated output on every character. A large value must stay linear and correct. + final int size = 1_000_000; + final char[] data = new char[size]; + Arrays.fill(data, 'a'); + final CSVFormat format = CSVFormat.RFC4180.withEscape('?').withDelimiter(',').withQuote(null).withRecordSeparator(CRLF); + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + final StringBuilder out = new StringBuilder(size); + format.print(new StringReader(new String(data)), out, true); + // No character in the value is special, so the output is the value unchanged. + assertEquals(size, out.length()); + }); + } + @Test void testPrintWithoutQuotes() throws IOException { final Reader in = new StringReader("");