From 68b4b0910be1b9c0beb52a39b906851f0c41c75c Mon Sep 17 00:00:00 2001 From: OldTruckDriver Date: Tue, 9 Jun 2026 21:15:13 +1000 Subject: [PATCH 01/42] [CSV-326] Escape Reader values with quote and escape --- src/changes/changes.xml | 1 + .../java/org/apache/commons/csv/CSVFormat.java | 7 ++++--- .../org/apache/commons/csv/CSVFormatTest.java | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 633de96bd..a3e03e372 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -51,6 +51,7 @@ CSVFormat.Builder.setQuote() does not refresh quotedNullString (#2447). Lexer.isDelimiter() accepts a partial multi-character delimiter at EOF (#603). CSVParser applies characterOffset to bytePosition (#604). + CSVPrinter Reader printing with quote and escape can emit CSV that its parser cannot read back. Add an "Android Compatibility" section to the web site. Add CSVParser.Builder.setByteOffset(long) (#604). diff --git a/src/main/java/org/apache/commons/csv/CSVFormat.java b/src/main/java/org/apache/commons/csv/CSVFormat.java index f6b2c5ae0..852a3956c 100644 --- a/src/main/java/org/apache/commons/csv/CSVFormat.java +++ b/src/main/java/org/apache/commons/csv/CSVFormat.java @@ -2522,14 +2522,15 @@ private void printWithQuotes(final Reader reader, final Appendable appendable) t return; } final char quote = getQuoteCharacter().charValue(); // Explicit unboxing is intentional + final char escape = isEscapeCharacterSet() ? getEscapeChar() : quote; // (1) Append opening quote append(quote, appendable); - // (2) Append Reader contents, doubling quotes + // (2) Append Reader contents, doubling quotes and escape characters int c; while (EOF != (c = reader.read())) { append((char) c, appendable); - if (c == quote) { - append(quote, appendable); + if (c == quote || c == escape) { + append((char) c, appendable); } } // (3) Append closing quote diff --git a/src/test/java/org/apache/commons/csv/CSVFormatTest.java b/src/test/java/org/apache/commons/csv/CSVFormatTest.java index ca18754f7..c3fdeeb77 100644 --- a/src/test/java/org/apache/commons/csv/CSVFormatTest.java +++ b/src/test/java/org/apache/commons/csv/CSVFormatTest.java @@ -966,6 +966,23 @@ void testPrintWithQuotes() throws IOException { assertEquals("\"\"\"a,b,c\r\nx,y,z\"", out.toString()); } + /** + * Tests CSV-326. + */ + @Test + void testPrintWithQuotesEscapeBeforeQuote() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder() + .setEscape('\\') + .setQuote('"') + .get(); + final String value = "\\\""; + final Appendable out = new StringBuilder(); + format.print(new StringReader(value), out, true); + try (CSVParser parser = CSVParser.parse(out.toString(), format)) { + assertEquals(value, parser.getRecords().get(0).get(0)); + } + } + @Test void testQuoteCharSameAsCommentStartThrowsException() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.builder().setQuote('!').setCommentMarker('!').get()); From 966b38519e45c1fd85c76fbbdc47cb5bb1905238 Mon Sep 17 00:00:00 2001 From: OldTruckDriver Date: Tue, 9 Jun 2026 21:25:28 +1000 Subject: [PATCH 02/42] [CSV-327] Limit parser maxRows by produced records --- src/changes/changes.xml | 1 + .../java/org/apache/commons/csv/CSVParser.java | 6 +++++- .../org/apache/commons/csv/CSVParserTest.java | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 633de96bd..44f1a6be0 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -51,6 +51,7 @@ CSVFormat.Builder.setQuote() does not refresh quotedNullString (#2447). Lexer.isDelimiter() accepts a partial multi-character delimiter at EOF (#603). CSVParser applies characterOffset to bytePosition (#604). + CSVParser applies maxRows to record numbers instead of rows produced when setRecordNumber(...) is used. Add an "Android Compatibility" section to the web site. Add CSVParser.Builder.setByteOffset(long) (#604). diff --git a/src/main/java/org/apache/commons/csv/CSVParser.java b/src/main/java/org/apache/commons/csv/CSVParser.java index c9b2dc44f..83b60170e 100644 --- a/src/main/java/org/apache/commons/csv/CSVParser.java +++ b/src/main/java/org/apache/commons/csv/CSVParser.java @@ -237,6 +237,7 @@ public Builder setTrackBytes(final boolean trackBytes) { final class CSVRecordIterator implements Iterator { private CSVRecord current; + private long recordCount; /** * Gets the next record or null at the end of stream or max rows read. @@ -247,8 +248,11 @@ final class CSVRecordIterator implements Iterator { */ private CSVRecord getNextRecord() { CSVRecord record = null; - if (format.useRow(recordNumber + 1)) { + if (format.useRow(recordCount + 1)) { record = Uncheck.get(CSVParser.this::nextRecord); + if (record != null) { + recordCount++; + } } return record; } diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index 8b1527c42..816c1c853 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -965,6 +965,23 @@ void testGetRecordsMaxRows(final long maxRows) throws IOException { } } + /** + * Tests CSV-327. + */ + @Test + void testGetRecordsMaxRowsWithRecordNumberOffset() throws IOException { + try (CSVParser parser = CSVParser.builder() + .setReader(new StringReader("a,b\nc,d\n")) + .setFormat(CSVFormat.DEFAULT.builder().setMaxRows(1).get()) + .setRecordNumber(2) + .get()) { + final List records = parser.getRecords(); + assertEquals(1, records.size()); + assertEquals(2, records.get(0).getRecordNumber()); + assertValuesEquals(new String[] { "a", "b" }, records.get(0)); + } + } + @Test void testGetRecordThreeBytesRead() throws Exception { final String code = "id,date,val5,val4\n" + From 1e3de1274636959d3cf70acbba14ae10128369a5 Mon Sep 17 00:00:00 2001 From: rootvector2 Date: Tue, 9 Jun 2026 17:37:44 +0530 Subject: [PATCH 03/42] clear escape delimiter buffer before peek in isEscapeDelimiter --- src/main/java/org/apache/commons/csv/Lexer.java | 1 + .../java/org/apache/commons/csv/CSVParserTest.java | 14 ++++++++++++++ .../java/org/apache/commons/csv/LexerTest.java | 12 ++++++++++++ 3 files changed, 27 insertions(+) diff --git a/src/main/java/org/apache/commons/csv/Lexer.java b/src/main/java/org/apache/commons/csv/Lexer.java index de97868e4..238e64cee 100644 --- a/src/main/java/org/apache/commons/csv/Lexer.java +++ b/src/main/java/org/apache/commons/csv/Lexer.java @@ -191,6 +191,7 @@ boolean isEscape(final int ch) { * @throws IOException If an I/O error occurs. */ boolean isEscapeDelimiter() throws IOException { + Arrays.fill(escapeDelimiterBuf, '\0'); reader.peek(escapeDelimiterBuf); if (escapeDelimiterBuf[0] != delimiter[0]) { return false; diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index 8b1527c42..5443c5e84 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -1665,6 +1665,20 @@ void testPartialMultiCharacterDelimiterAtEOF() throws IOException { } } + /** + * A truncated escaped multi-character delimiter at EOF must stay literal data and not be completed from a stale + * escape delimiter look-ahead. + */ + @Test + void testPartialEscapedMultiCharacterDelimiterAtEOF() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").setEscape('!').get(); + try (CSVParser parser = format.parse(new StringReader("x![!|!]y![!|"))) { + final CSVRecord record = parser.nextRecord(); + assertEquals("x[|]y![!|", record.get(0)); + assertEquals(1, record.size()); + } + } + @Test void testProvidedHeader() throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); diff --git a/src/test/java/org/apache/commons/csv/LexerTest.java b/src/test/java/org/apache/commons/csv/LexerTest.java index 511876a28..da60df07e 100644 --- a/src/test/java/org/apache/commons/csv/LexerTest.java +++ b/src/test/java/org/apache/commons/csv/LexerTest.java @@ -421,6 +421,18 @@ void testPartialMultiCharacterDelimiterAtEOF() throws IOException { } } + /** + * A truncated escaped multi-character delimiter at EOF must not be accepted by reusing the previous escape delimiter + * look-ahead in {@link Lexer#isEscapeDelimiter()}. + */ + @Test + void testPartialEscapedMultiCharacterDelimiterAtEOF() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").setEscape('!').get(); + try (Lexer lexer = createLexer("x![!|!]y![!|", format)) { + assertNextToken(EOF, "x[|]y![!|", lexer); + } + } + @Test void testReadEscapeBackspace() throws IOException { try (Lexer lexer = createLexer("b", CSVFormat.DEFAULT.withEscape('\b'))) { From 4f9a4037a2c1890154e1f077d66306ec54afbf60 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Thu, 11 Jun 2026 09:40:09 -0400 Subject: [PATCH 04/42] Bump org.apache.commons:commons-parent from 101 to 102. --- pom.xml | 2 +- src/changes/changes.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 57bca27b2..8cb13ed7c 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ org.apache.commons commons-parent - 101 + 102 commons-csv 1.15.0-SNAPSHOT diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 633de96bd..87d68ea2a 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -55,7 +55,7 @@ Add an "Android Compatibility" section to the web site. Add CSVParser.Builder.setByteOffset(long) (#604). - Bump org.apache.commons:commons-parent from 85 to 101 #573, #595. + Bump org.apache.commons:commons-parent from 85 to 102 #573, #595. [test] Bump com.opencsv:opencsv from 5.11.2 to 5.12.0 #558. Bump org.apache.commons:commons-lang3 from 3.18.0 to 3.20.0. Bump commons-codec:commons-codec from 1.19.0 to 1.22.0. From 27126657d2117afd40e8972b8a34659abc753a65 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Thu, 11 Jun 2026 14:23:37 -0400 Subject: [PATCH 05/42] Update legacy GitHub links in CONTRIBUTING.md --- CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb15f2518..3423e18ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,13 +48,13 @@ Getting Started --------------- + Make sure you have a [JIRA account](https://issues.apache.org/jira/). -+ Make sure you have a [GitHub account](https://github.com/signup/free). This is not essential, but makes providing patches much easier. ++ Make sure you have a [GitHub account](https://github.com/signup). This is not essential, but makes providing patches much easier. + If you're planning to implement a new feature it makes sense to discuss your changes on the [dev list](https://commons.apache.org/mail-lists.html) first. This way you can make sure you're not wasting your time on something that isn't considered to be in Apache Commons CSV's scope. + Submit a [Jira Ticket][jira] for your issue, assuming one does not already exist. + Clearly describe the issue including steps to reproduce when it is a bug. + Make sure you fill in the earliest version that you know has the issue. + Find the corresponding [repository on GitHub](https://github.com/apache/?query=commons-), -[fork](https://help.github.com/articles/fork-a-repo/) and check out your forked repository. If you don't have a GitHub account, you can still clone the Commons repository. +[fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) and check out your forked repository. If you don't have a GitHub account, you can still clone the Commons repository. Making Changes -------------- @@ -108,8 +108,8 @@ Additional Resources + [Contributing patches](https://commons.apache.org/patches.html) + [Apache Commons CSV JIRA project page][jira] + [Contributor License Agreement][cla] -+ [General GitHub documentation](https://help.github.com/) -+ [GitHub pull request documentation](https://help.github.com/articles/creating-a-pull-request/) ++ [General GitHub documentation](https://docs.github.com/) ++ [GitHub pull request documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) + [Apache Commons Twitter Account](https://twitter.com/ApacheCommons) [cla]:https://www.apache.org/licenses/#clas From 6887303cbca84216a3324e103504d9dd91660ea8 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Thu, 11 Jun 2026 15:33:31 -0400 Subject: [PATCH 06/42] [CSV-326] Escape Reader values with quote and escape (#606). --- src/changes/changes.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 8ea12d983..2475f2b9b 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -53,6 +53,8 @@ CSVParser applies characterOffset to bytePosition (#604). CSVPrinter Reader printing with quote and escape can emit CSV that its parser cannot read back. CSVParser applies maxRows to record numbers instead of rows produced when setRecordNumber(...) is used. + Escape Reader values with quote and escape (#606). +. Add an "Android Compatibility" section to the web site. Add CSVParser.Builder.setByteOffset(long) (#604). From 037e2e0cd161c9d4f485aae8e49879d6cf2048ab Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Thu, 11 Jun 2026 15:59:34 -0400 Subject: [PATCH 07/42] Fix typo. --- src/changes/changes.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 2475f2b9b..cb4a17848 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -53,8 +53,7 @@ CSVParser applies characterOffset to bytePosition (#604). CSVPrinter Reader printing with quote and escape can emit CSV that its parser cannot read back. CSVParser applies maxRows to record numbers instead of rows produced when setRecordNumber(...) is used. - Escape Reader values with quote and escape (#606). -. + Escape Reader values with quote and escape (#606). Add an "Android Compatibility" section to the web site. Add CSVParser.Builder.setByteOffset(long) (#604). From 19b29139dfdb58426bf1330567e6d6c750abe81c Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Thu, 11 Jun 2026 16:00:26 -0400 Subject: [PATCH 08/42] Clear escape delimiter buffer before peek in isEscapeDelimiter (#608). --- src/changes/changes.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index cb4a17848..0786cc365 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -54,6 +54,7 @@ CSVPrinter Reader printing with quote and escape can emit CSV that its parser cannot read back. CSVParser applies maxRows to record numbers instead of rows produced when setRecordNumber(...) is used. Escape Reader values with quote and escape (#606). + Clear escape delimiter buffer before peek in isEscapeDelimiter (#608). Add an "Android Compatibility" section to the web site. Add CSVParser.Builder.setByteOffset(long) (#604). From b5940a642eef0de550733887fc5b78451d4a8eed Mon Sep 17 00:00:00 2001 From: rootvector2 Date: Sat, 13 Jun 2026 12:30:53 +0530 Subject: [PATCH 09/42] escape quote char in printWithEscapes when QuoteMode is NONE --- .../java/org/apache/commons/csv/CSVFormat.java | 8 ++++++-- .../org/apache/commons/csv/CSVPrinterTest.java | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/csv/CSVFormat.java b/src/main/java/org/apache/commons/csv/CSVFormat.java index 852a3956c..03211e689 100644 --- a/src/main/java/org/apache/commons/csv/CSVFormat.java +++ b/src/main/java/org/apache/commons/csv/CSVFormat.java @@ -2324,12 +2324,14 @@ private void printWithEscapes(final CharSequence charSeq, final Appendable appen final char[] delimArray = getDelimiterCharArray(); final int delimLength = delimArray.length; final char escape = getEscapeChar(); + final boolean quoteSet = isQuoteCharacterSet(); + final char quote = quoteSet ? getQuoteCharacter().charValue() : 0; while (pos < end) { char c = charSeq.charAt(pos); final boolean isDelimiterStart = isDelimiter(c, charSeq, pos, delimArray, delimLength); final boolean isCr = c == Constants.CR; final boolean isLf = c == Constants.LF; - if (isCr || isLf || c == escape || isDelimiterStart) { + if (isCr || isLf || c == escape || quoteSet && c == quote || isDelimiterStart) { // write out segment up until this char if (pos > start) { appendable.append(charSeq, start, pos); @@ -2368,6 +2370,8 @@ private void printWithEscapes(final Reader reader, final Appendable appendable) final char[] delimArray = getDelimiterCharArray(); final int delimLength = delimArray.length; final char escape = getEscapeChar(); + final boolean quoteSet = isQuoteCharacterSet(); + final char quote = quoteSet ? getQuoteCharacter().charValue() : 0; final StringBuilder builder = new StringBuilder(IOUtils.DEFAULT_BUFFER_SIZE); int c; final char[] lookAheadBuffer = new char[delimLength - 1]; @@ -2379,7 +2383,7 @@ private void printWithEscapes(final Reader reader, final Appendable appendable) final boolean isDelimiterStart = isDelimiter((char) c, test, pos, delimArray, delimLength); final boolean isCr = c == Constants.CR; final boolean isLf = c == Constants.LF; - if (isCr || isLf || c == escape || isDelimiterStart) { + if (isCr || isLf || c == escape || quoteSet && c == quote || isDelimiterStart) { // write out segment up until this char if (pos > start) { append(builder.substring(start, pos), appendable); diff --git a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java index 1ff791010..7d1993e01 100644 --- a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java +++ b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java @@ -423,6 +423,23 @@ void testDelimeterStringQuoteNone() throws IOException { } } + @Test + void testQuoteCharEscapedWithQuoteModeNone() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setQuote('"').setEscape('?').setQuoteMode(QuoteMode.NONE).get(); + final StringWriter sw = new StringWriter(); + try (CSVPrinter printer = new CSVPrinter(sw, format)) { + printer.printRecord("\"abc", "x\"y"); + } + assertEquals("?\"abc,x?\"y\r\n", sw.toString()); + // The emitted record must read back as the original values. + try (CSVParser parser = CSVParser.parse(sw.toString(), format)) { + final List records = parser.getRecords(); + assertEquals(1, records.size()); + assertEquals("\"abc", records.get(0).get(0)); + assertEquals("x\"y", records.get(0).get(1)); + } + } + @Test void testDelimiterEscaped() throws IOException { final StringWriter sw = new StringWriter(); From 27a439ae0aba41221d296bca7bb5e00379bc25a8 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Sat, 13 Jun 2026 08:33:49 -0400 Subject: [PATCH 10/42] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../java/org/apache/commons/csv/CSVPrinterTest.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java index 7d1993e01..79ce987bd 100644 --- a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java +++ b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java @@ -429,14 +429,17 @@ void testQuoteCharEscapedWithQuoteModeNone() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, format)) { printer.printRecord("\"abc", "x\"y"); + printer.printRecord(new StringReader("\"abc"), new StringReader("x\"y")); } - assertEquals("?\"abc,x?\"y\r\n", sw.toString()); - // The emitted record must read back as the original values. + assertEquals("?\"abc,x?\"y" + RECORD_SEPARATOR + "?\"abc,x?\"y" + RECORD_SEPARATOR, sw.toString()); + // The emitted records must read back as the original values. try (CSVParser parser = CSVParser.parse(sw.toString(), format)) { final List records = parser.getRecords(); - assertEquals(1, records.size()); - assertEquals("\"abc", records.get(0).get(0)); - assertEquals("x\"y", records.get(0).get(1)); + assertEquals(2, records.size()); + for (final CSVRecord record : records) { + assertEquals("\"abc", record.get(0)); + assertEquals("x\"y", record.get(1)); + } } } From d729b442e4bbdf6c603e3e64955d27352744cc29 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Sat, 13 Jun 2026 12:37:26 +0000 Subject: [PATCH 11/42] Sort members --- .../org/apache/commons/csv/CSVParserTest.java | 28 ++++++------- .../apache/commons/csv/CSVPrinterTest.java | 40 +++++++++---------- .../org/apache/commons/csv/LexerTest.java | 24 +++++------ 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index cccbef4e6..dca37fc5a 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -1668,20 +1668,6 @@ void testParsingPrintedEmptyFirstColumn(final CSVFormat.Predefined format) throw } } - /** - * Tests CSV-324. - */ - @Test - void testPartialMultiCharacterDelimiterAtEOF() throws IOException { - final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").get(); - try (CSVParser parser = format.parse(new StringReader("a[|]b[|"))) { - final CSVRecord record = parser.nextRecord(); - assertEquals("a", record.get(0)); - assertEquals("b[|", record.get(1)); - assertEquals(2, record.size()); - } - } - /** * A truncated escaped multi-character delimiter at EOF must stay literal data and not be completed from a stale * escape delimiter look-ahead. @@ -1696,6 +1682,20 @@ void testPartialEscapedMultiCharacterDelimiterAtEOF() throws IOException { } } + /** + * Tests CSV-324. + */ + @Test + void testPartialMultiCharacterDelimiterAtEOF() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").get(); + try (CSVParser parser = format.parse(new StringReader("a[|]b[|"))) { + final CSVRecord record = parser.nextRecord(); + assertEquals("a", record.get(0)); + assertEquals("b[|", record.get(1)); + assertEquals(2, record.size()); + } + } + @Test void testProvidedHeader() throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); diff --git a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java index 79ce987bd..b58782210 100644 --- a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java +++ b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java @@ -423,26 +423,6 @@ void testDelimeterStringQuoteNone() throws IOException { } } - @Test - void testQuoteCharEscapedWithQuoteModeNone() throws IOException { - final CSVFormat format = CSVFormat.DEFAULT.builder().setQuote('"').setEscape('?').setQuoteMode(QuoteMode.NONE).get(); - final StringWriter sw = new StringWriter(); - try (CSVPrinter printer = new CSVPrinter(sw, format)) { - printer.printRecord("\"abc", "x\"y"); - printer.printRecord(new StringReader("\"abc"), new StringReader("x\"y")); - } - assertEquals("?\"abc,x?\"y" + RECORD_SEPARATOR + "?\"abc,x?\"y" + RECORD_SEPARATOR, sw.toString()); - // The emitted records must read back as the original values. - try (CSVParser parser = CSVParser.parse(sw.toString(), format)) { - final List records = parser.getRecords(); - assertEquals(2, records.size()); - for (final CSVRecord record : records) { - assertEquals("\"abc", record.get(0)); - assertEquals("x\"y", record.get(1)); - } - } - } - @Test void testDelimiterEscaped() throws IOException { final StringWriter sw = new StringWriter(); @@ -1818,6 +1798,26 @@ void testQuoteAll() throws IOException { } } + @Test + void testQuoteCharEscapedWithQuoteModeNone() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setQuote('"').setEscape('?').setQuoteMode(QuoteMode.NONE).get(); + final StringWriter sw = new StringWriter(); + try (CSVPrinter printer = new CSVPrinter(sw, format)) { + printer.printRecord("\"abc", "x\"y"); + printer.printRecord(new StringReader("\"abc"), new StringReader("x\"y")); + } + assertEquals("?\"abc,x?\"y" + RECORD_SEPARATOR + "?\"abc,x?\"y" + RECORD_SEPARATOR, sw.toString()); + // The emitted records must read back as the original values. + try (CSVParser parser = CSVParser.parse(sw.toString(), format)) { + final List records = parser.getRecords(); + assertEquals(2, records.size()); + for (final CSVRecord record : records) { + assertEquals("\"abc", record.get(0)); + assertEquals("x\"y", record.get(1)); + } + } + } + @Test void testQuoteCommaFirstChar() throws IOException { final StringWriter sw = new StringWriter(); diff --git a/src/test/java/org/apache/commons/csv/LexerTest.java b/src/test/java/org/apache/commons/csv/LexerTest.java index da60df07e..244079df6 100644 --- a/src/test/java/org/apache/commons/csv/LexerTest.java +++ b/src/test/java/org/apache/commons/csv/LexerTest.java @@ -409,18 +409,6 @@ void testNextToken6() throws IOException { } } - /** - * Tests CSV-324. - */ - @Test - void testPartialMultiCharacterDelimiterAtEOF() throws IOException { - final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").get(); - try (Lexer lexer = createLexer("a[|]b[|", format)) { - assertNextToken(TOKEN, "a", lexer); - assertNextToken(EOF, "b[|", lexer); - } - } - /** * A truncated escaped multi-character delimiter at EOF must not be accepted by reusing the previous escape delimiter * look-ahead in {@link Lexer#isEscapeDelimiter()}. @@ -433,6 +421,18 @@ void testPartialEscapedMultiCharacterDelimiterAtEOF() throws IOException { } } + /** + * Tests CSV-324. + */ + @Test + void testPartialMultiCharacterDelimiterAtEOF() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").get(); + try (Lexer lexer = createLexer("a[|]b[|", format)) { + assertNextToken(TOKEN, "a", lexer); + assertNextToken(EOF, "b[|", lexer); + } + } + @Test void testReadEscapeBackspace() throws IOException { try (Lexer lexer = createLexer("b", CSVFormat.DEFAULT.withEscape('\b'))) { From d4d4154454b43c46aff65ed75c721605eafb142b Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Sat, 13 Jun 2026 12:43:32 +0000 Subject: [PATCH 12/42] Refactor some magic strings in CSVPrinterTest.testQuoteCharEscapedWithQuoteModeNone() --- .../java/org/apache/commons/csv/CSVPrinterTest.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java index b58782210..16901c4e2 100644 --- a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java +++ b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java @@ -1802,9 +1802,11 @@ void testQuoteAll() throws IOException { void testQuoteCharEscapedWithQuoteModeNone() throws IOException { final CSVFormat format = CSVFormat.DEFAULT.builder().setQuote('"').setEscape('?').setQuoteMode(QuoteMode.NONE).get(); final StringWriter sw = new StringWriter(); + final String col1 = "\"abc"; + final String col2 = "x\"y"; try (CSVPrinter printer = new CSVPrinter(sw, format)) { - printer.printRecord("\"abc", "x\"y"); - printer.printRecord(new StringReader("\"abc"), new StringReader("x\"y")); + printer.printRecord(col1, col2); + printer.printRecord(new StringReader(col1), new StringReader(col2)); } assertEquals("?\"abc,x?\"y" + RECORD_SEPARATOR + "?\"abc,x?\"y" + RECORD_SEPARATOR, sw.toString()); // The emitted records must read back as the original values. @@ -1812,8 +1814,8 @@ void testQuoteCharEscapedWithQuoteModeNone() throws IOException { final List records = parser.getRecords(); assertEquals(2, records.size()); for (final CSVRecord record : records) { - assertEquals("\"abc", record.get(0)); - assertEquals("x\"y", record.get(1)); + assertEquals(col1, record.get(0)); + assertEquals(col2, record.get(1)); } } } From 411d3a37c923361f2ccca1c26862e6e4d7fd4742 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Sat, 13 Jun 2026 12:45:52 +0000 Subject: [PATCH 13/42] Escape quote char in printWithEscapes when QuoteMode is NONE (#609). --- src/changes/changes.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 0786cc365..41b1a038c 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -55,6 +55,7 @@ CSVParser applies maxRows to record numbers instead of rows produced when setRecordNumber(...) is used. Escape Reader values with quote and escape (#606). Clear escape delimiter buffer before peek in isEscapeDelimiter (#608). + Escape quote char in printWithEscapes when QuoteMode is NONE (#609). Add an "Android Compatibility" section to the web site. Add CSVParser.Builder.setByteOffset(long) (#604). From a99f2609299a72b08a3e43c2968822555528da4e Mon Sep 17 00:00:00 2001 From: rootvector2 Date: Mon, 15 Jun 2026 21:31:34 +0530 Subject: [PATCH 14/42] quote value starting with comment marker in minimal quote mode --- .../java/org/apache/commons/csv/CSVFormat.java | 5 +++-- .../org/apache/commons/csv/CSVPrinterTest.java | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/csv/CSVFormat.java b/src/main/java/org/apache/commons/csv/CSVFormat.java index 03211e689..4f60eff93 100644 --- a/src/main/java/org/apache/commons/csv/CSVFormat.java +++ b/src/main/java/org/apache/commons/csv/CSVFormat.java @@ -2454,10 +2454,11 @@ private void printWithQuotes(final Object object, final CharSequence charSeq, fi } } else { char c = charSeq.charAt(pos); - if (c <= Constants.COMMENT) { + if (c <= Constants.COMMENT || isCommentMarkerSet() && c == commentMarker.charValue()) { // Some other chars at the start of a value caused the parser to fail, so for now // encapsulate if we start in anything less than '#'. We are being conservative - // by including the default comment char too. + // by including the default comment char and any configured comment marker too, + // which the parser would otherwise read back as a comment line. quote = true; } else { while (pos < len) { diff --git a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java index 16901c4e2..e00accfb0 100644 --- a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java +++ b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java @@ -1829,6 +1829,24 @@ void testQuoteCommaFirstChar() throws IOException { } } + @Test + void testQuoteCommentMarkerFirstChar() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setCommentMarker(';').get(); + final StringWriter sw = new StringWriter(); + final String col1 = ";comment-like"; + try (CSVPrinter printer = new CSVPrinter(sw, format)) { + printer.printRecord(col1, "b"); + } + assertEquals("\";comment-like\",b" + RECORD_SEPARATOR, sw.toString()); + // A value starting with the comment marker must read back as data, not a dropped comment line. + try (CSVParser parser = CSVParser.parse(sw.toString(), format)) { + final List records = parser.getRecords(); + assertEquals(1, records.size()); + assertEquals(col1, records.get(0).get(0)); + assertEquals("b", records.get(0).get(1)); + } + } + @Test void testQuoteNonNumeric() throws IOException { final StringWriter sw = new StringWriter(); From 3c2291cf5e40c5b9a19f3a6b3165fb27c8de5321 Mon Sep 17 00:00:00 2001 From: rootvector2 Date: Tue, 16 Jun 2026 14:35:39 +0530 Subject: [PATCH 15/42] expand comment marker test to contrast printed comment with quoted value --- .../org/apache/commons/csv/CSVPrinterTest.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java index e00accfb0..f4f3c85b1 100644 --- a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java +++ b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java @@ -1835,15 +1835,24 @@ void testQuoteCommentMarkerFirstChar() throws IOException { final StringWriter sw = new StringWriter(); final String col1 = ";comment-like"; try (CSVPrinter printer = new CSVPrinter(sw, format)) { + // A real comment is written with the marker, unquoted. + printer.printComment("a real comment"); + // A value starting with the marker is quoted, so it does not read back as a comment. printer.printRecord(col1, "b"); + // The marker past the first character does not start a comment, so only the leading-marker value is quoted. + printer.printRecord("a;b", ";c"); } - assertEquals("\";comment-like\",b" + RECORD_SEPARATOR, sw.toString()); - // A value starting with the comment marker must read back as data, not a dropped comment line. + assertEquals("; a real comment" + RECORD_SEPARATOR + + "\";comment-like\",b" + RECORD_SEPARATOR + + "a;b,\";c\"" + RECORD_SEPARATOR, sw.toString()); + // The comment is dropped on read; both data records survive intact. try (CSVParser parser = CSVParser.parse(sw.toString(), format)) { final List records = parser.getRecords(); - assertEquals(1, records.size()); + assertEquals(2, records.size()); assertEquals(col1, records.get(0).get(0)); assertEquals("b", records.get(0).get(1)); + assertEquals("a;b", records.get(1).get(0)); + assertEquals(";c", records.get(1).get(1)); } } From e21d66e410cdafca2e822361de5eb6b2596291f2 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Tue, 16 Jun 2026 21:20:03 +0000 Subject: [PATCH 16/42] Quote value starting with comment marker in minimal quote mode (#610). Extract to local variable. --- src/changes/changes.xml | 1 + src/test/java/org/apache/commons/csv/CSVPrinterTest.java | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 41b1a038c..431da6b5f 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -56,6 +56,7 @@ Escape Reader values with quote and escape (#606). Clear escape delimiter buffer before peek in isEscapeDelimiter (#608). Escape quote char in printWithEscapes when QuoteMode is NONE (#609). + Quote value starting with comment marker in minimal quote mode (#610).. Add an "Android Compatibility" section to the web site. Add CSVParser.Builder.setByteOffset(long) (#604). diff --git a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java index f4f3c85b1..e68d4c243 100644 --- a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java +++ b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java @@ -1842,11 +1842,12 @@ void testQuoteCommentMarkerFirstChar() throws IOException { // The marker past the first character does not start a comment, so only the leading-marker value is quoted. printer.printRecord("a;b", ";c"); } + final String string = sw.toString(); assertEquals("; a real comment" + RECORD_SEPARATOR + "\";comment-like\",b" + RECORD_SEPARATOR + - "a;b,\";c\"" + RECORD_SEPARATOR, sw.toString()); + "a;b,\";c\"" + RECORD_SEPARATOR, string); // The comment is dropped on read; both data records survive intact. - try (CSVParser parser = CSVParser.parse(sw.toString(), format)) { + try (CSVParser parser = CSVParser.parse(string, format)) { final List records = parser.getRecords(); assertEquals(2, records.size()); assertEquals(col1, records.get(0).get(0)); From 110e830616e44844a0a57256401093a151ae0e66 Mon Sep 17 00:00:00 2001 From: rootvector2 Date: Wed, 17 Jun 2026 16:38:10 +0530 Subject: [PATCH 17/42] clear delimiter buffer before each peek in isDelimiter --- src/main/java/org/apache/commons/csv/Lexer.java | 2 +- src/test/java/org/apache/commons/csv/LexerTest.java | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apache/commons/csv/Lexer.java b/src/main/java/org/apache/commons/csv/Lexer.java index 238e64cee..93a584663 100644 --- a/src/main/java/org/apache/commons/csv/Lexer.java +++ b/src/main/java/org/apache/commons/csv/Lexer.java @@ -153,6 +153,7 @@ boolean isDelimiter(final int ch) throws IOException { isLastTokenDelimiter = true; return true; } + Arrays.fill(delimiterBuf, '\0'); reader.peek(delimiterBuf); for (int i = 0; i < delimiterBuf.length; i++) { if (delimiterBuf[i] != delimiter[i + 1]) { @@ -274,7 +275,6 @@ Token nextToken(final Token token) throws IOException { token.type = Token.Type.COMMENT; return token; } - Arrays.fill(delimiterBuf, '\0'); // Important: make sure a new char gets consumed in each iteration while (token.type == Token.Type.INVALID) { // ignore whitespaces at beginning of a token diff --git a/src/test/java/org/apache/commons/csv/LexerTest.java b/src/test/java/org/apache/commons/csv/LexerTest.java index 244079df6..e5f831fb2 100644 --- a/src/test/java/org/apache/commons/csv/LexerTest.java +++ b/src/test/java/org/apache/commons/csv/LexerTest.java @@ -433,6 +433,19 @@ void testPartialMultiCharacterDelimiterAtEOF() throws IOException { } } + /** + * A truncated multi-character delimiter at EOF must not be accepted by reusing the look-ahead buffer left dirty by an + * earlier non-matching peek in the same token (CSV-324 only cleared the buffer once per token). + */ + @Test + void testPartialMultiCharacterDelimiterAtEOFAfterMismatch() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").get(); + // The "[a]" peek leaves ']' in the look-ahead buffer; the trailing "[|" must not match "[|]". + try (Lexer lexer = createLexer("x[a][|", format)) { + assertNextToken(EOF, "x[a][|", lexer); + } + } + @Test void testReadEscapeBackspace() throws IOException { try (Lexer lexer = createLexer("b", CSVFormat.DEFAULT.withEscape('\b'))) { From 61f521350b34c5605bfd68760cdce120a5da4ed7 Mon Sep 17 00:00:00 2001 From: rootvector2 Date: Wed, 17 Jun 2026 17:07:51 +0530 Subject: [PATCH 18/42] add public-api parser test for partial delimiter false-match at eof --- .../org/apache/commons/csv/CSVParserTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index dca37fc5a..c1ca3d7a4 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -1696,6 +1696,21 @@ void testPartialMultiCharacterDelimiterAtEOF() throws IOException { } } + /** + * A truncated multi-character delimiter at EOF must not be completed from the look-ahead buffer left dirty by an + * earlier non-matching peek in the same token. + */ + @Test + void testPartialMultiCharacterDelimiterAtEOFAfterMismatch() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").get(); + // The "[a]" peek leaves ']' in the look-ahead buffer; the trailing "[|" must not match "[|]". + try (CSVParser parser = format.parse(new StringReader("x[a][|"))) { + final CSVRecord record = parser.nextRecord(); + assertEquals("x[a][|", record.get(0)); + assertEquals(1, record.size()); + } + } + @Test void testProvidedHeader() throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); From ed8dbf25ad73856cfa10cba4f5e9855fdcae0d88 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Wed, 17 Jun 2026 12:08:58 +0000 Subject: [PATCH 19/42] Clear escape delimiter buffer before peek in Lexer.isEscapeDelimiter() (#608, #611). Refactor magic strings in tests --- src/changes/changes.xml | 2 +- src/test/java/org/apache/commons/csv/CSVParserTest.java | 5 +++-- src/test/java/org/apache/commons/csv/LexerTest.java | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 431da6b5f..66073c9dd 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -54,7 +54,7 @@ CSVPrinter Reader printing with quote and escape can emit CSV that its parser cannot read back. CSVParser applies maxRows to record numbers instead of rows produced when setRecordNumber(...) is used. Escape Reader values with quote and escape (#606). - Clear escape delimiter buffer before peek in isEscapeDelimiter (#608). + Clear escape delimiter buffer before peek in Lexer.isEscapeDelimiter() (#608, #611). Escape quote char in printWithEscapes when QuoteMode is NONE (#609). Quote value starting with comment marker in minimal quote mode (#610).. diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index c1ca3d7a4..5bece571f 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -1704,9 +1704,10 @@ void testPartialMultiCharacterDelimiterAtEOF() throws IOException { void testPartialMultiCharacterDelimiterAtEOFAfterMismatch() throws IOException { final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").get(); // The "[a]" peek leaves ']' in the look-ahead buffer; the trailing "[|" must not match "[|]". - try (CSVParser parser = format.parse(new StringReader("x[a][|"))) { + final String recordString = "x[a][|"; + try (CSVParser parser = format.parse(new StringReader(recordString))) { final CSVRecord record = parser.nextRecord(); - assertEquals("x[a][|", record.get(0)); + assertEquals(recordString, record.get(0)); assertEquals(1, record.size()); } } diff --git a/src/test/java/org/apache/commons/csv/LexerTest.java b/src/test/java/org/apache/commons/csv/LexerTest.java index e5f831fb2..db1ab3a6d 100644 --- a/src/test/java/org/apache/commons/csv/LexerTest.java +++ b/src/test/java/org/apache/commons/csv/LexerTest.java @@ -441,8 +441,9 @@ void testPartialMultiCharacterDelimiterAtEOF() throws IOException { void testPartialMultiCharacterDelimiterAtEOFAfterMismatch() throws IOException { final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").get(); // The "[a]" peek leaves ']' in the look-ahead buffer; the trailing "[|" must not match "[|]". - try (Lexer lexer = createLexer("x[a][|", format)) { - assertNextToken(EOF, "x[a][|", lexer); + final String recordString = "x[a][|"; + try (Lexer lexer = createLexer(recordString, format)) { + assertNextToken(EOF, recordString, lexer); } } From a6ee67ecf0d4b9208ffc640f433d8b40c258e1f3 Mon Sep 17 00:00:00 2001 From: OldTruckDriver Date: Fri, 19 Jun 2026 01:32:16 +1000 Subject: [PATCH 20/42] [CSV-328] Fix quoted null string after disabling quote setNullString(String) rebuilt quotedNullString by concatenating the nullable quoteCharacter field directly, so calling setQuote(null) before setNullString(...) produced a literal "nullNULLnull". Extract a shared setQuotedNullString() helper that applies the default-quote fallback, so both builder orders produce the same state. Reviewed-by: OpenAI Codex Reviewed-by: Anthropic Claude Code --- src/changes/changes.xml | 1 + src/main/java/org/apache/commons/csv/CSVFormat.java | 7 +++++-- src/test/java/org/apache/commons/csv/CSVFormatTest.java | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 66073c9dd..64f936554 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -53,6 +53,7 @@ CSVParser applies characterOffset to bytePosition (#604). CSVPrinter Reader printing with quote and escape can emit CSV that its parser cannot read back. CSVParser applies maxRows to record numbers instead of rows produced when setRecordNumber(...) is used. + CSVFormat.Builder.setNullString(String) can build an invalid quoted null string after setQuote(null). Escape Reader values with quote and escape (#606). Clear escape delimiter buffer before peek in Lexer.isEscapeDelimiter() (#608, #611). Escape quote char in printWithEscapes when QuoteMode is NONE (#609). diff --git a/src/main/java/org/apache/commons/csv/CSVFormat.java b/src/main/java/org/apache/commons/csv/CSVFormat.java index 4f60eff93..9c403d9e1 100644 --- a/src/main/java/org/apache/commons/csv/CSVFormat.java +++ b/src/main/java/org/apache/commons/csv/CSVFormat.java @@ -780,8 +780,7 @@ public Builder setMaxRows(final long maxRows) { */ public Builder setNullString(final String nullString) { this.nullString = nullString; - this.quotedNullString = quoteCharacter + nullString + quoteCharacter; - return this; + return setQuotedNullString(); } /** @@ -806,6 +805,10 @@ public Builder setQuote(final Character quoteCharacter) { throw new IllegalArgumentException("The quoteCharacter cannot be a line break"); } this.quoteCharacter = quoteCharacter; + return setQuotedNullString(); + } + + private Builder setQuotedNullString() { final Character quote = quoteCharacter != null ? quoteCharacter : Constants.DOUBLE_QUOTE_CHAR; this.quotedNullString = quote + nullString + quote; return this; diff --git a/src/test/java/org/apache/commons/csv/CSVFormatTest.java b/src/test/java/org/apache/commons/csv/CSVFormatTest.java index c3fdeeb77..ed20898de 100644 --- a/src/test/java/org/apache/commons/csv/CSVFormatTest.java +++ b/src/test/java/org/apache/commons/csv/CSVFormatTest.java @@ -1040,6 +1040,11 @@ void testQuotedNullStringTracksQuoteCharacter() throws IOException { builder.setQuote((Character) null); builder.get().print(null, out, true); assertEquals("\"NULL\"", out.toString()); + // reset, reverse setter order + out.setLength(0); + builder.setNullString(null).setQuote((Character) null).setNullString("NULL"); + builder.get().print(null, out, true); + assertEquals("\"NULL\"", out.toString()); } @Test From 1d89cd5f0aa454ef3853dfc7528242399ef26b74 Mon Sep 17 00:00:00 2001 From: OldTruckDriver Date: Fri, 19 Jun 2026 02:05:55 +1000 Subject: [PATCH 21/42] [CSV-329] Fix byte tracking for supplementary delimiters ExtendedBufferedReader.read(char[], int, int) updated lastChar before computing the encoded byte length, so a surrogate pair in the delimiter lookahead buffer was paired against the post-update lastChar and threw CharacterCodingException. Count bytes before updating lastChar, and pair each char against the preceding char in the buffer seeded from lastChar so pairs split across reads still count. Add parser and ExtendedBufferedReader regression tests. Reviewed-by: OpenAI Codex Reviewed-by: Anthropic Claude Code --- src/changes/changes.xml | 1 + .../commons/csv/ExtendedBufferedReader.java | 20 +++++++++------ .../org/apache/commons/csv/CSVParserTest.java | 25 +++++++++++++++++++ .../csv/ExtendedBufferedReaderTest.java | 14 +++++++++++ 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 66073c9dd..f6a474dbf 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -53,6 +53,7 @@ CSVParser applies characterOffset to bytePosition (#604). CSVPrinter Reader printing with quote and escape can emit CSV that its parser cannot read back. CSVParser applies maxRows to record numbers instead of rows produced when setRecordNumber(...) is used. + CSVParser with trackBytes enabled throws on multi-character delimiters containing supplementary Unicode characters. Escape Reader values with quote and escape (#606). Clear escape delimiter buffer before peek in Lexer.isEscapeDelimiter() (#608, #611). Escape quote char in printWithEscapes when QuoteMode is NONE (#609). diff --git a/src/main/java/org/apache/commons/csv/ExtendedBufferedReader.java b/src/main/java/org/apache/commons/csv/ExtendedBufferedReader.java index 889b58edc..5b519a08c 100644 --- a/src/main/java/org/apache/commons/csv/ExtendedBufferedReader.java +++ b/src/main/java/org/apache/commons/csv/ExtendedBufferedReader.java @@ -108,9 +108,11 @@ long getBytesRead() { } private long getEncodedCharLength(final char[] buf, final int offset, final int length) throws CharacterCodingException { - int len = 0; - for (int i = offset; i < length; i++) { - len += getEncodedCharLength(buf[i]); + long len = 0; + int previous = lastChar; + for (int i = offset; i < offset + length; i++) { + len += getEncodedCharLength(previous, buf[i]); + previous = buf[i]; } return len; } @@ -141,8 +143,12 @@ private long getEncodedCharLength(final char[] buf, final int offset, final int * @throws CharacterCodingException if the character cannot be encoded. */ private int getEncodedCharLength(final int current) throws CharacterCodingException { + return getEncodedCharLength(lastChar, current); + } + + private int getEncodedCharLength(final int previous, final int current) throws CharacterCodingException { final char cChar = (char) current; - final char lChar = (char) lastChar; + final char lChar = (char) previous; if (!Character.isSurrogate(cChar)) { return encoder.encode(CharBuffer.wrap(new char[] { cChar })).limit(); } @@ -218,6 +224,9 @@ public int read(final char[] buf, final int offset, final int length) throws IOE return 0; } final int len = super.read(buf, offset, length); + if (encoder != null && len > 0) { + this.bytesRead += getEncodedCharLength(buf, offset, len); + } if (len > 0) { for (int i = offset; i < offset + len; i++) { final char ch = buf[i]; @@ -233,9 +242,6 @@ public int read(final char[] buf, final int offset, final int length) throws IOE } else if (len == EOF) { lastChar = EOF; } - if (encoder != null) { - this.bytesRead += getEncodedCharLength(buf, offset, len); - } position += len; return len; } diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index 5bece571f..29ca0cf1f 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -666,6 +666,31 @@ void testGetBytePositionMultiCharacterDelimiter() throws IOException { } } + /** + * Tests CSV-329. + */ + @Test + void testGetBytePositionMultiCharacterDelimiterWithSupplementaryCharacter() throws IOException { + final String delimiter = "x😀"; + final String code = "ax😀b\ncx😀d\n"; + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter(delimiter).get(); + try (CSVParser parser = CSVParser.builder() + .setReader(new StringReader(code)) + .setFormat(format) + .setCharset(UTF_8) + .setTrackBytes(true) + .get()) { + final CSVRecord first = parser.nextRecord(); + final CSVRecord second = parser.nextRecord(); + assertNotNull(first); + assertNotNull(second); + assertValuesEquals(new String[] { "a", "b" }, first); + assertValuesEquals(new String[] { "c", "d" }, second); + assertEquals(0, first.getBytePosition()); + assertEquals("ax😀b\n".getBytes(UTF_8).length, second.getBytePosition()); + } + } + @Test void testGetBytePositionWithCharacterOffsetAndMultiBytePrefix() throws Exception { final String row0 = "é,x\n"; diff --git a/src/test/java/org/apache/commons/csv/ExtendedBufferedReaderTest.java b/src/test/java/org/apache/commons/csv/ExtendedBufferedReaderTest.java index 056b8a9c9..b8d9b9f19 100644 --- a/src/test/java/org/apache/commons/csv/ExtendedBufferedReaderTest.java +++ b/src/test/java/org/apache/commons/csv/ExtendedBufferedReaderTest.java @@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import java.io.StringReader; +import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; @@ -104,6 +105,19 @@ void testReadingInDifferentBuffer() throws Exception { } } + @Test + void testReadingSupplementaryCharacterTracksBytes() throws Exception { + final String input = "😀"; + final char[] buffer = new char[input.length()]; + try (ExtendedBufferedReader reader = new ExtendedBufferedReader(new StringReader(input), StandardCharsets.UTF_8, true)) { + assertEquals(input.length(), reader.read(buffer, 0, buffer.length)); + assertArrayEquals(input.toCharArray(), buffer); + assertEquals(input.getBytes(StandardCharsets.UTF_8).length, reader.getBytesRead()); + assertEquals(input.length(), reader.getPosition()); + assertEquals(input.charAt(input.length() - 1), reader.getLastChar()); + } + } + @Test void testReadLine() throws Exception { try (ExtendedBufferedReader br = createBufferedReader("")) { From d8e12423b47109c76196bbae454726a114cf7c07 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Fri, 19 Jun 2026 07:28:18 -0400 Subject: [PATCH 22/42] Bump actions/checkout from 6.0.3 to 7.0.0. --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/maven.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 57f55cae0..08c673ee0 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae #v5.0.5 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 114f3d8a2..7bc02bdd2 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -26,6 +26,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: 'Dependency Review PR' uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 3ee3dec2b..3cb743cbf 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -43,7 +43,7 @@ jobs: experimental: true steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae #v5.0.5 From a1cf4f2a73065281ca7c22841c2f3ec0c00098c1 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Fri, 19 Jun 2026 11:44:21 +0000 Subject: [PATCH 23/42] Refactor delimiter in test Rename local variable --- src/test/java/org/apache/commons/csv/CSVParserTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index 29ca0cf1f..aa4a639dd 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -672,10 +672,10 @@ void testGetBytePositionMultiCharacterDelimiter() throws IOException { @Test void testGetBytePositionMultiCharacterDelimiterWithSupplementaryCharacter() throws IOException { final String delimiter = "x😀"; - final String code = "ax😀b\ncx😀d\n"; + final String data = "a" + delimiter + "b\nc" + delimiter + "d\n"; final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter(delimiter).get(); try (CSVParser parser = CSVParser.builder() - .setReader(new StringReader(code)) + .setReader(new StringReader(data)) .setFormat(format) .setCharset(UTF_8) .setTrackBytes(true) @@ -687,7 +687,7 @@ void testGetBytePositionMultiCharacterDelimiterWithSupplementaryCharacter() thro assertValuesEquals(new String[] { "a", "b" }, first); assertValuesEquals(new String[] { "c", "d" }, second); assertEquals(0, first.getBytePosition()); - assertEquals("ax😀b\n".getBytes(UTF_8).length, second.getBytePosition()); + assertEquals("a" + delimiter + "b\n".getBytes(UTF_8).length, second.getBytePosition()); } } From caa1c8d0ed1a05f4a75f9a4fc6e5e2dc6fa5bf51 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Fri, 19 Jun 2026 18:51:44 +0000 Subject: [PATCH 24/42] Revert "Refactor delimiter in test" This reverts commit a1cf4f2a73065281ca7c22841c2f3ec0c00098c1. --- src/test/java/org/apache/commons/csv/CSVParserTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index aa4a639dd..29ca0cf1f 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -672,10 +672,10 @@ void testGetBytePositionMultiCharacterDelimiter() throws IOException { @Test void testGetBytePositionMultiCharacterDelimiterWithSupplementaryCharacter() throws IOException { final String delimiter = "x😀"; - final String data = "a" + delimiter + "b\nc" + delimiter + "d\n"; + final String code = "ax😀b\ncx😀d\n"; final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter(delimiter).get(); try (CSVParser parser = CSVParser.builder() - .setReader(new StringReader(data)) + .setReader(new StringReader(code)) .setFormat(format) .setCharset(UTF_8) .setTrackBytes(true) @@ -687,7 +687,7 @@ void testGetBytePositionMultiCharacterDelimiterWithSupplementaryCharacter() thro assertValuesEquals(new String[] { "a", "b" }, first); assertValuesEquals(new String[] { "c", "d" }, second); assertEquals(0, first.getBytePosition()); - assertEquals("a" + delimiter + "b\n".getBytes(UTF_8).length, second.getBytePosition()); + assertEquals("ax😀b\n".getBytes(UTF_8).length, second.getBytePosition()); } } From 61aa0555d60c68120914b4952232f8c6bfc72ed3 Mon Sep 17 00:00:00 2001 From: rootvector2 Date: Fri, 19 Jun 2026 23:39:51 +0530 Subject: [PATCH 25/42] escape leading comment marker in printWithEscapes --- .../org/apache/commons/csv/CSVFormat.java | 14 ++++- .../apache/commons/csv/CSVPrinterTest.java | 51 +++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/csv/CSVFormat.java b/src/main/java/org/apache/commons/csv/CSVFormat.java index 9c403d9e1..eaa8c8ffe 100644 --- a/src/main/java/org/apache/commons/csv/CSVFormat.java +++ b/src/main/java/org/apache/commons/csv/CSVFormat.java @@ -2329,12 +2329,16 @@ private void printWithEscapes(final CharSequence charSeq, final Appendable appen final char escape = getEscapeChar(); final boolean quoteSet = isQuoteCharacterSet(); final char quote = quoteSet ? getQuoteCharacter().charValue() : 0; + final boolean commentMarkerSet = isCommentMarkerSet(); + final char commentChar = commentMarkerSet ? commentMarker.charValue() : 0; // Explicit unboxing is intentional while (pos < end) { char c = charSeq.charAt(pos); final boolean isDelimiterStart = isDelimiter(c, charSeq, pos, delimArray, delimLength); final boolean isCr = c == Constants.CR; final boolean isLf = c == Constants.LF; - if (isCr || isLf || c == escape || quoteSet && c == quote || isDelimiterStart) { + // A leading comment marker would be read back as a comment, so escape it. + final boolean isComment = commentMarkerSet && pos == 0 && c == commentChar; + if (isCr || isLf || c == escape || quoteSet && c == quote || isDelimiterStart || isComment) { // write out segment up until this char if (pos > start) { appendable.append(charSeq, start, pos); @@ -2375,8 +2379,11 @@ private void printWithEscapes(final Reader reader, final Appendable appendable) final char escape = getEscapeChar(); final boolean quoteSet = isQuoteCharacterSet(); final char quote = quoteSet ? getQuoteCharacter().charValue() : 0; + final boolean commentMarkerSet = isCommentMarkerSet(); + final char commentChar = commentMarkerSet ? commentMarker.charValue() : 0; // Explicit unboxing is intentional final StringBuilder builder = new StringBuilder(IOUtils.DEFAULT_BUFFER_SIZE); int c; + boolean firstChar = true; final char[] lookAheadBuffer = new char[delimLength - 1]; while (EOF != (c = bufferedReader.read())) { builder.append((char) c); @@ -2386,7 +2393,10 @@ private void printWithEscapes(final Reader reader, final Appendable appendable) final boolean isDelimiterStart = isDelimiter((char) c, test, pos, delimArray, delimLength); final boolean isCr = c == Constants.CR; final boolean isLf = c == Constants.LF; - if (isCr || isLf || c == escape || quoteSet && c == quote || isDelimiterStart) { + // A leading comment marker would be read back as a comment, so escape it. + final boolean isComment = commentMarkerSet && firstChar && c == commentChar; + firstChar = false; + if (isCr || isLf || c == escape || quoteSet && c == quote || isDelimiterStart || isComment) { // write out segment up until this char if (pos > start) { append(builder.substring(start, pos), appendable); diff --git a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java index e68d4c243..9ae80c1e5 100644 --- a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java +++ b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java @@ -569,6 +569,57 @@ void testEscapeBackslash5() throws IOException { assertEquals("\\\\", sw.toString()); } + @Test + void testEscapeCommentMarkerFirstChar() throws IOException { + // No quoting available in escape mode, so a leading comment marker must be escaped or the + // record reads back as a comment and is dropped. Mirrors the quoting fix for QuoteMode.MINIMAL. + final CSVFormat format = CSVFormat.DEFAULT.builder().setQuote(null).setEscape('\\').setCommentMarker(';').get(); + final StringWriter sw = new StringWriter(); + final String col1 = ";comment-like"; + try (CSVPrinter printer = new CSVPrinter(sw, format)) { + printer.printRecord(col1, "b"); + printer.printRecord(new StringReader(col1), new StringReader("b")); + // The marker past the first character does not start a comment and is left alone. + printer.printRecord("a;b", ";c"); + } + final String string = sw.toString(); + assertEquals("\\;comment-like,b" + RECORD_SEPARATOR + + "\\;comment-like,b" + RECORD_SEPARATOR + + "a;b,\\;c" + RECORD_SEPARATOR, string); + // The emitted records must read back as the original values, none parsed as a comment. + try (CSVParser parser = CSVParser.parse(string, format)) { + final List records = parser.getRecords(); + assertEquals(3, records.size()); + assertEquals(col1, records.get(0).get(0)); + assertEquals("b", records.get(0).get(1)); + assertEquals(col1, records.get(1).get(0)); + assertEquals("b", records.get(1).get(1)); + assertEquals("a;b", records.get(2).get(0)); + assertEquals(";c", records.get(2).get(1)); + } + } + + @Test + void testEscapeCommentMarkerFirstCharWithQuoteModeNone() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setEscape('\\').setQuoteMode(QuoteMode.NONE).setCommentMarker(';').get(); + final StringWriter sw = new StringWriter(); + final String col1 = ";bar"; + try (CSVPrinter printer = new CSVPrinter(sw, format)) { + printer.printRecord(col1, "b"); + printer.printRecord(new StringReader(col1), new StringReader("b")); + } + final String string = sw.toString(); + assertEquals("\\;bar,b" + RECORD_SEPARATOR + "\\;bar,b" + RECORD_SEPARATOR, string); + try (CSVParser parser = CSVParser.parse(string, format)) { + final List records = parser.getRecords(); + assertEquals(2, records.size()); + for (final CSVRecord record : records) { + assertEquals(col1, record.get(0)); + assertEquals("b", record.get(1)); + } + } + } + @Test void testEscapeNull1() throws IOException { final StringWriter sw = new StringWriter(); From b112daacc74664c925b28d413b660dc47faddcef Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Fri, 19 Jun 2026 21:14:05 +0000 Subject: [PATCH 26/42] Escape leading comment marker in printWithEscapes (#614). --- src/changes/changes.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 006de7711..f172a96fe 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -58,7 +58,8 @@ Escape Reader values with quote and escape (#606). Clear escape delimiter buffer before peek in Lexer.isEscapeDelimiter() (#608, #611). Escape quote char in printWithEscapes when QuoteMode is NONE (#609). - Quote value starting with comment marker in minimal quote mode (#610).. + Quote value starting with comment marker in minimal quote mode (#610). + Escape leading comment marker in printWithEscapes (#614). Add an "Android Compatibility" section to the web site. Add CSVParser.Builder.setByteOffset(long) (#604). From 274b4ceba4e418726a5c9e7043bf9d460b0429c5 Mon Sep 17 00:00:00 2001 From: rootvector2 Date: Sat, 20 Jun 2026 20:33:44 +0530 Subject: [PATCH 27/42] skip byte counting at EOF in ExtendedBufferedReader.read --- .../commons/csv/ExtendedBufferedReader.java | 2 +- .../org/apache/commons/csv/CSVParserTest.java | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apache/commons/csv/ExtendedBufferedReader.java b/src/main/java/org/apache/commons/csv/ExtendedBufferedReader.java index 5b519a08c..20c1ef544 100644 --- a/src/main/java/org/apache/commons/csv/ExtendedBufferedReader.java +++ b/src/main/java/org/apache/commons/csv/ExtendedBufferedReader.java @@ -210,7 +210,7 @@ public int read() throws IOException { if (current == CR || current == LF && lastChar != CR || current == EOF && lastChar != CR && lastChar != LF && lastChar != EOF) { lineNumber++; } - if (encoder != null) { + if (encoder != null && current != EOF) { this.bytesRead += getEncodedCharLength(current); } lastChar = current; diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index 29ca0cf1f..1332fa582 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -691,6 +691,27 @@ void testGetBytePositionMultiCharacterDelimiterWithSupplementaryCharacter() thro } } + @Test + void testGetBytePositionWithSingleByteCharset() throws IOException { + // A single-byte charset cannot encode U+FFFF, the char value of the EOF sentinel. + // Byte counting must skip the EOF read so a valid file parses without throwing. + final String code = "a,b\nc,d\n"; + try (CSVParser parser = CSVParser.builder() + .setReader(new StringReader(code)) + .setFormat(CSVFormat.DEFAULT) + .setCharset(StandardCharsets.ISO_8859_1) + .setTrackBytes(true) + .get()) { + final CSVRecord first = parser.nextRecord(); + final CSVRecord second = parser.nextRecord(); + assertNotNull(first); + assertNotNull(second); + assertNull(parser.nextRecord()); + assertEquals(0, first.getBytePosition()); + assertEquals(4, second.getBytePosition()); + } + } + @Test void testGetBytePositionWithCharacterOffsetAndMultiBytePrefix() throws Exception { final String row0 = "é,x\n"; From 0633c989c9ff892d99bacd3289a3ff8d4cb0fbd6 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Sat, 20 Jun 2026 15:11:11 +0000 Subject: [PATCH 28/42] Skip byte counting at EOF in ExtendedBufferedReader.read (#615). Sort members. --- src/changes/changes.xml | 1 + .../org/apache/commons/csv/CSVParserTest.java | 42 +++++++++---------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index f172a96fe..867a20507 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -60,6 +60,7 @@ Escape quote char in printWithEscapes when QuoteMode is NONE (#609). Quote value starting with comment marker in minimal quote mode (#610). Escape leading comment marker in printWithEscapes (#614). + Skip byte counting at EOF in ExtendedBufferedReader.read (#615). Add an "Android Compatibility" section to the web site. Add CSVParser.Builder.setByteOffset(long) (#604). diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index 1332fa582..309a073cf 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -691,27 +691,6 @@ void testGetBytePositionMultiCharacterDelimiterWithSupplementaryCharacter() thro } } - @Test - void testGetBytePositionWithSingleByteCharset() throws IOException { - // A single-byte charset cannot encode U+FFFF, the char value of the EOF sentinel. - // Byte counting must skip the EOF read so a valid file parses without throwing. - final String code = "a,b\nc,d\n"; - try (CSVParser parser = CSVParser.builder() - .setReader(new StringReader(code)) - .setFormat(CSVFormat.DEFAULT) - .setCharset(StandardCharsets.ISO_8859_1) - .setTrackBytes(true) - .get()) { - final CSVRecord first = parser.nextRecord(); - final CSVRecord second = parser.nextRecord(); - assertNotNull(first); - assertNotNull(second); - assertNull(parser.nextRecord()); - assertEquals(0, first.getBytePosition()); - assertEquals(4, second.getBytePosition()); - } - } - @Test void testGetBytePositionWithCharacterOffsetAndMultiBytePrefix() throws Exception { final String row0 = "é,x\n"; @@ -742,6 +721,27 @@ void testGetBytePositionWithCharacterOffsetAndMultiBytePrefix() throws Exception } } + @Test + void testGetBytePositionWithSingleByteCharset() throws IOException { + // A single-byte charset cannot encode U+FFFF, the char value of the EOF sentinel. + // Byte counting must skip the EOF read so a valid file parses without throwing. + final String code = "a,b\nc,d\n"; + try (CSVParser parser = CSVParser.builder() + .setReader(new StringReader(code)) + .setFormat(CSVFormat.DEFAULT) + .setCharset(StandardCharsets.ISO_8859_1) + .setTrackBytes(true) + .get()) { + final CSVRecord first = parser.nextRecord(); + final CSVRecord second = parser.nextRecord(); + assertNotNull(first); + assertNotNull(second); + assertNull(parser.nextRecord()); + assertEquals(0, first.getBytePosition()); + assertEquals(4, second.getBytePosition()); + } + } + @Test void testGetHeaderComment_HeaderComment1() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_HEADER_COMMENT, FORMAT_AUTO_HEADER)) { From 609a228e35adf9d73275a6b88e065de091c94be6 Mon Sep 17 00:00:00 2001 From: rootvector2 Date: Mon, 22 Jun 2026 02:01:19 +0530 Subject: [PATCH 29/42] keep quoted empty trailing field with trailingDelimiter --- .../java/org/apache/commons/csv/CSVParser.java | 4 +++- .../org/apache/commons/csv/CSVParserTest.java | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apache/commons/csv/CSVParser.java b/src/main/java/org/apache/commons/csv/CSVParser.java index 83b60170e..141eba732 100644 --- a/src/main/java/org/apache/commons/csv/CSVParser.java +++ b/src/main/java/org/apache/commons/csv/CSVParser.java @@ -580,7 +580,9 @@ public CSVParser(final Reader reader, final CSVFormat format, final long charact private void addRecordValue(final boolean lastRecord) { final String input = format.trim(reusableToken.content.toString()); - if (lastRecord && input.isEmpty() && format.getTrailingDelimiter()) { + // Only drop the empty field produced by an actual trailing delimiter. A quoted empty + // field ("") is a real value, not a trailing delimiter, so it must be kept. + if (lastRecord && input.isEmpty() && format.getTrailingDelimiter() && !reusableToken.isQuoted) { return; } recordList.add(handleNull(input)); diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index 309a073cf..051548757 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -1949,6 +1949,23 @@ void testTrailingDelimiter() throws Exception { } } + @Test + void testTrailingDelimiterKeepsQuotedEmptyLastField() throws Exception { + final CSVFormat format = CSVFormat.DEFAULT.builder().setTrailingDelimiter(true).get(); + try (CSVParser parser = CSVParser.parse("a,b,\"\"", format)) { + final CSVRecord record = parser.iterator().next(); + assertEquals(3, record.size()); + assertEquals("a", record.get(0)); + assertEquals("b", record.get(1)); + assertEquals("", record.get(2)); + } + // An unquoted trailing delimiter still drops the empty field. + try (CSVParser parser = CSVParser.parse("a,b,", format)) { + final CSVRecord record = parser.iterator().next(); + assertEquals(2, record.size()); + } + } + @Test void testTrim() throws Exception { final Reader in = new StringReader("a,a,a\n\" 1 \",\" 2 \",\" 3 \"\nx,y,z"); From 53360c47dbdd5d6ba4fe5e8008f8bb3510200b33 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Mon, 22 Jun 2026 07:32:06 -0400 Subject: [PATCH 30/42] Bump actions/setup-java from 5.2.0 to 5.3.0 --- .github/workflows/maven.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 3cb743cbf..a6154ddb1 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -53,7 +53,7 @@ jobs: restore-keys: | ${{ runner.os }}-maven- - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 with: distribution: ${{ runner.os == 'macOS' && matrix.java == '8' && 'zulu' || 'temurin' }} java-version: ${{ matrix.java }} From fc4c0e3b43c3cba69f023d336629d2696e250294 Mon Sep 17 00:00:00 2001 From: rootvector2 Date: Mon, 22 Jun 2026 18:06:33 +0530 Subject: [PATCH 31/42] document trailing delimiter parse behavior and contrast with trailing data --- .../org/apache/commons/csv/CSVFormat.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/org/apache/commons/csv/CSVFormat.java b/src/main/java/org/apache/commons/csv/CSVFormat.java index eaa8c8ffe..7145d23d3 100644 --- a/src/main/java/org/apache/commons/csv/CSVFormat.java +++ b/src/main/java/org/apache/commons/csv/CSVFormat.java @@ -883,6 +883,16 @@ public Builder setTrailingData(final boolean trailingData) { /** * Sets whether to add a trailing delimiter. * + *

+ * When writing, a delimiter is appended after the last value of each record. When reading, the empty field + * that such a trailing delimiter produces is dropped so the output round-trips back to the original record; + * a quoted empty trailing field ({@code ""}) is a real value rather than a trailing delimiter and is kept. + *

+ *

+ * This is unrelated to {@link #setTrailingData(boolean) trailing data}, which controls whether characters + * after the closing quote of an encapsulated value are tolerated when reading. + *

+ * * @param trailingDelimiter whether to add a trailing delimiter. * @return This instance. */ @@ -2012,6 +2022,16 @@ public boolean getTrailingData() { /** * Gets whether to add a trailing delimiter. * + *

+ * When writing, a delimiter is appended after the last value of each record. When reading, the empty field + * that such a trailing delimiter produces is dropped so the output round-trips back to the original record; + * a quoted empty trailing field ({@code ""}) is a real value rather than a trailing delimiter and is kept. + *

+ *

+ * This is unrelated to {@link #getTrailingData() trailing data}, which controls whether characters after the + * closing quote of an encapsulated value are tolerated when reading. + *

+ * * @return whether to add a trailing delimiter. * @since 1.3 */ From e729d17d5089d794c07e41dd6c9d374f9ea09e85 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Mon, 22 Jun 2026 13:49:46 +0000 Subject: [PATCH 32/42] Keep quoted empty trailing field with trailingDelimiter (#616). --- src/changes/changes.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 867a20507..0d0175ccc 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -61,6 +61,7 @@ Quote value starting with comment marker in minimal quote mode (#610). Escape leading comment marker in printWithEscapes (#614). Skip byte counting at EOF in ExtendedBufferedReader.read (#615). + Keep quoted empty trailing field with trailingDelimiter (#616). Add an "Android Compatibility" section to the web site. Add CSVParser.Builder.setByteOffset(long) (#604). From afbf34ad92cbf741b9dfad84699e890d8695a6f9 Mon Sep 17 00:00:00 2001 From: Naveed Khan Date: Thu, 25 Jun 2026 23:12:11 +0530 Subject: [PATCH 33/42] evaluate isDelimiter once in nextToken whitespace skip --- .../java/org/apache/commons/csv/Lexer.java | 11 +++++++++-- .../org/apache/commons/csv/LexerTest.java | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/csv/Lexer.java b/src/main/java/org/apache/commons/csv/Lexer.java index 93a584663..fe964480a 100644 --- a/src/main/java/org/apache/commons/csv/Lexer.java +++ b/src/main/java/org/apache/commons/csv/Lexer.java @@ -277,15 +277,22 @@ Token nextToken(final Token token) throws IOException { } // Important: make sure a new char gets consumed in each iteration while (token.type == Token.Type.INVALID) { + // isDelimiter consumes the trailing characters of a multi-character delimiter as a side effect, so it must + // only be evaluated once per character. Remember a match found while skipping whitespace below. + boolean delimiter = false; // ignore whitespaces at beginning of a token if (ignoreSurroundingSpaces) { - while (Character.isWhitespace((char) c) && !isDelimiter(c) && !eol) { + while (Character.isWhitespace((char) c) && !eol) { + if (isDelimiter(c)) { + delimiter = true; + break; + } c = reader.read(); eol = readEndOfLine(c); } } // ok, start of token reached: encapsulated, or token - if (isDelimiter(c)) { + if (delimiter || isDelimiter(c)) { // empty token return TOKEN("") token.type = Token.Type.TOKEN; } else if (eol) { diff --git a/src/test/java/org/apache/commons/csv/LexerTest.java b/src/test/java/org/apache/commons/csv/LexerTest.java index db1ab3a6d..445f710a1 100644 --- a/src/test/java/org/apache/commons/csv/LexerTest.java +++ b/src/test/java/org/apache/commons/csv/LexerTest.java @@ -447,6 +447,25 @@ void testPartialMultiCharacterDelimiterAtEOFAfterMismatch() throws IOException { } } + /** + * With {@code ignoreSurroundingSpaces} enabled and a multi-character delimiter whose first character is whitespace, + * the side-effecting {@link Lexer#isDelimiter(int)} must only be evaluated once per character, otherwise the + * delimiter is consumed in the whitespace-skip loop and the empty field at the boundary is dropped. + */ + @Test + void testEmptyTokenBeforeWhitespacePrefixedMultiCharacterDelimiter() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter(" |").setIgnoreSurroundingSpaces(true).get(); + try (Lexer lexer = createLexer(" |a", format)) { + assertNextToken(TOKEN, "", lexer); + assertNextToken(EOF, "a", lexer); + } + try (Lexer lexer = createLexer("a | |b", format)) { + assertNextToken(TOKEN, "a", lexer); + assertNextToken(TOKEN, "", lexer); + assertNextToken(EOF, "b", lexer); + } + } + @Test void testReadEscapeBackspace() throws IOException { try (Lexer lexer = createLexer("b", CSVFormat.DEFAULT.withEscape('\b'))) { From c9362f76baa32534e82334a27220b698db49788c Mon Sep 17 00:00:00 2001 From: Naveed Khan Date: Fri, 26 Jun 2026 02:45:35 +0530 Subject: [PATCH 34/42] add public-api test for whitespace-prefixed multi-char delimiter exercises the empty-field-dropped regression through CSVParser, not just the lexer. --- .../org/apache/commons/csv/CSVParserTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index 051548757..565e132eb 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -1758,6 +1758,26 @@ void testPartialMultiCharacterDelimiterAtEOFAfterMismatch() throws IOException { } } + /** + * With {@code ignoreSurroundingSpaces} enabled and a multi-character delimiter whose first character is whitespace, + * the empty field at the delimiter boundary must survive. The delimiter look-ahead is consumed while skipping + * leading whitespace, so re-evaluating it would drop the empty field and merge the following field's value. + */ + @Test + void testEmptyFieldBeforeWhitespacePrefixedMultiCharacterDelimiter() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter(" |").setIgnoreSurroundingSpaces(true).get(); + try (CSVParser parser = CSVParser.parse(" |a", format)) { + final List records = parser.getRecords(); + assertEquals(1, records.size()); + assertValuesEquals(new String[] { "", "a" }, records.get(0)); + } + try (CSVParser parser = CSVParser.parse("a | |b", format)) { + final List records = parser.getRecords(); + assertEquals(1, records.size()); + assertValuesEquals(new String[] { "a", "", "b" }, records.get(0)); + } + } + @Test void testProvidedHeader() throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); From c4113ceb3acfba0634133e47d35c40d141e18525 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Thu, 25 Jun 2026 22:11:14 +0000 Subject: [PATCH 35/42] Evaluate isDelimiter once in nextToken whitespace skip (#618). --- src/changes/changes.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 0d0175ccc..93952e9f1 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -62,6 +62,7 @@ Escape leading comment marker in printWithEscapes (#614). Skip byte counting at EOF in ExtendedBufferedReader.read (#615). Keep quoted empty trailing field with trailingDelimiter (#616). + Evaluate isDelimiter once in nextToken whitespace skip (#618).. Add an "Android Compatibility" section to the web site. Add CSVParser.Builder.setByteOffset(long) (#604). From e36e7f3a1d0fbe29f2ff602f041d3a3d4195b84a Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Thu, 25 Jun 2026 22:11:53 +0000 Subject: [PATCH 36/42] Sort members --- .../org/apache/commons/csv/CSVParserTest.java | 40 +++++++++---------- .../org/apache/commons/csv/LexerTest.java | 38 +++++++++--------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index 565e132eb..3bea08fac 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -465,6 +465,26 @@ void testDuplicateHeadersNotAllowed() { () -> CSVParser.parse("a,b,a\n1,2,3\nx,y,z", CSVFormat.DEFAULT.withHeader().withAllowDuplicateHeaderNames(false))); } + /** + * With {@code ignoreSurroundingSpaces} enabled and a multi-character delimiter whose first character is whitespace, + * the empty field at the delimiter boundary must survive. The delimiter look-ahead is consumed while skipping + * leading whitespace, so re-evaluating it would drop the empty field and merge the following field's value. + */ + @Test + void testEmptyFieldBeforeWhitespacePrefixedMultiCharacterDelimiter() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter(" |").setIgnoreSurroundingSpaces(true).get(); + try (CSVParser parser = CSVParser.parse(" |a", format)) { + final List records = parser.getRecords(); + assertEquals(1, records.size()); + assertValuesEquals(new String[] { "", "a" }, records.get(0)); + } + try (CSVParser parser = CSVParser.parse("a | |b", format)) { + final List records = parser.getRecords(); + assertEquals(1, records.size()); + assertValuesEquals(new String[] { "a", "", "b" }, records.get(0)); + } + } + @Test void testEmptyFile() throws Exception { try (CSVParser parser = CSVParser.parse(Paths.get("src/test/resources/org/apache/commons/csv/empty.txt"), StandardCharsets.UTF_8, @@ -1758,26 +1778,6 @@ void testPartialMultiCharacterDelimiterAtEOFAfterMismatch() throws IOException { } } - /** - * With {@code ignoreSurroundingSpaces} enabled and a multi-character delimiter whose first character is whitespace, - * the empty field at the delimiter boundary must survive. The delimiter look-ahead is consumed while skipping - * leading whitespace, so re-evaluating it would drop the empty field and merge the following field's value. - */ - @Test - void testEmptyFieldBeforeWhitespacePrefixedMultiCharacterDelimiter() throws IOException { - final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter(" |").setIgnoreSurroundingSpaces(true).get(); - try (CSVParser parser = CSVParser.parse(" |a", format)) { - final List records = parser.getRecords(); - assertEquals(1, records.size()); - assertValuesEquals(new String[] { "", "a" }, records.get(0)); - } - try (CSVParser parser = CSVParser.parse("a | |b", format)) { - final List records = parser.getRecords(); - assertEquals(1, records.size()); - assertValuesEquals(new String[] { "a", "", "b" }, records.get(0)); - } - } - @Test void testProvidedHeader() throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); diff --git a/src/test/java/org/apache/commons/csv/LexerTest.java b/src/test/java/org/apache/commons/csv/LexerTest.java index 445f710a1..a76f6e513 100644 --- a/src/test/java/org/apache/commons/csv/LexerTest.java +++ b/src/test/java/org/apache/commons/csv/LexerTest.java @@ -216,6 +216,25 @@ void testDelimiterIsWhitespace() throws IOException { } } + /** + * With {@code ignoreSurroundingSpaces} enabled and a multi-character delimiter whose first character is whitespace, + * the side-effecting {@link Lexer#isDelimiter(int)} must only be evaluated once per character, otherwise the + * delimiter is consumed in the whitespace-skip loop and the empty field at the boundary is dropped. + */ + @Test + void testEmptyTokenBeforeWhitespacePrefixedMultiCharacterDelimiter() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter(" |").setIgnoreSurroundingSpaces(true).get(); + try (Lexer lexer = createLexer(" |a", format)) { + assertNextToken(TOKEN, "", lexer); + assertNextToken(EOF, "a", lexer); + } + try (Lexer lexer = createLexer("a | |b", format)) { + assertNextToken(TOKEN, "a", lexer); + assertNextToken(TOKEN, "", lexer); + assertNextToken(EOF, "b", lexer); + } + } + @Test void testEOFWithoutClosingQuote() throws Exception { final String code = "a,\"b"; @@ -447,25 +466,6 @@ void testPartialMultiCharacterDelimiterAtEOFAfterMismatch() throws IOException { } } - /** - * With {@code ignoreSurroundingSpaces} enabled and a multi-character delimiter whose first character is whitespace, - * the side-effecting {@link Lexer#isDelimiter(int)} must only be evaluated once per character, otherwise the - * delimiter is consumed in the whitespace-skip loop and the empty field at the boundary is dropped. - */ - @Test - void testEmptyTokenBeforeWhitespacePrefixedMultiCharacterDelimiter() throws IOException { - final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter(" |").setIgnoreSurroundingSpaces(true).get(); - try (Lexer lexer = createLexer(" |a", format)) { - assertNextToken(TOKEN, "", lexer); - assertNextToken(EOF, "a", lexer); - } - try (Lexer lexer = createLexer("a | |b", format)) { - assertNextToken(TOKEN, "a", lexer); - assertNextToken(TOKEN, "", lexer); - assertNextToken(EOF, "b", lexer); - } - } - @Test void testReadEscapeBackspace() throws IOException { try (Lexer lexer = createLexer("b", CSVFormat.DEFAULT.withEscape('\b'))) { From 26a53751934e52f84663e5e90956db99f0eefc49 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Thu, 25 Jun 2026 22:13:08 +0000 Subject: [PATCH 37/42] Add test more assertions to CSVParserTest.testEmptyFieldBeforeWhitespacePrefixedMultiCharacterDelimiter() --- src/test/java/org/apache/commons/csv/CSVParserTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index 3bea08fac..6d9bdd9e8 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -483,6 +483,11 @@ void testEmptyFieldBeforeWhitespacePrefixedMultiCharacterDelimiter() throws IOEx assertEquals(1, records.size()); assertValuesEquals(new String[] { "a", "", "b" }, records.get(0)); } + try (CSVParser parser = CSVParser.parse("a | |b |", format)) { + final List records = parser.getRecords(); + assertEquals(1, records.size()); + assertValuesEquals(new String[] { "a", "", "b", "" }, records.get(0)); + } } @Test From b346046e10a5833e5f6143fd0162155bb51ccc87 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Thu, 25 Jun 2026 18:23:59 -0400 Subject: [PATCH 38/42] Bump actions/cache from 5.0.5 to 6.0.0. --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/maven.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 08c673ee0..20f1ee2cb 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -49,7 +49,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae #v5.0.5 + - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index a6154ddb1..139df406d 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae #v5.0.5 + - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} From 930bc7919db7ebb81dc566e9e278ce2bed3acf42 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Mon, 29 Jun 2026 07:29:52 -0400 Subject: [PATCH 39/42] Bump actions/cache from 6.0.0 to 6.1.0 --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/maven.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 20f1ee2cb..cca38e512 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -49,7 +49,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 139df406d..2637840b1 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} From 6471196a31d9ea92942a634732e5350ef5253fcf Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Tue, 30 Jun 2026 13:58:31 +0000 Subject: [PATCH 40/42] Javadoc --- src/main/java/org/apache/commons/csv/CSVRecord.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/apache/commons/csv/CSVRecord.java b/src/main/java/org/apache/commons/csv/CSVRecord.java index 502bf318a..8dab14d90 100644 --- a/src/main/java/org/apache/commons/csv/CSVRecord.java +++ b/src/main/java/org/apache/commons/csv/CSVRecord.java @@ -281,7 +281,7 @@ public Iterator iterator() { /** * Puts all values of this record into the given Map. * - * @param the map type. + * @param The map type. * @param map The Map to populate. * @return the given map. * @since 1.9.0 From f7efa29cd9c4d9d1c1cafdae4469cf254bf22f42 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Tue, 30 Jun 2026 20:50:12 -0400 Subject: [PATCH 41/42] Bump actions/setup-java from 5.3.0 to 5.4.0 --- .github/workflows/maven.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 2637840b1..17ba7dd38 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -53,7 +53,7 @@ jobs: restore-keys: | ${{ runner.os }}-maven- - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0 + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: distribution: ${{ runner.os == 'macOS' && matrix.java == '8' && 'zulu' || 'temurin' }} java-version: ${{ matrix.java }} From 4434d93b92ff3f7b0754e65eba53721dd95c59f1 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Wed, 1 Jul 2026 07:31:54 -0400 Subject: [PATCH 42/42] Bump actions/checkout from 6.0.2 to 7.0.0 --- .github/workflows/scorecards-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index bf246c140..e1868cb46 100644 --- a/.github/workflows/scorecards-analysis.yml +++ b/.github/workflows/scorecards-analysis.yml @@ -40,7 +40,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0 with: persist-credentials: false