Skip to content

Commit 515899a

Browse files
authored
Merge pull request #625 from saleemno1/chunked-reader-lookahead
fill the lookahead buffer on short reads from a chunked source
2 parents 02acbed + baa697f commit 515899a

4 files changed

Lines changed: 130 additions & 1 deletion

File tree

src/main/java/org/apache/commons/csv/ExtendedBufferedReader.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,37 @@ public void mark(final int readAheadLimit) throws IOException {
225225
super.mark(readAheadLimit);
226226
}
227227

228+
/**
229+
* Fills {@code array} with the characters that follow the current position without consuming them.
230+
* <p>
231+
* Overridden because the inherited implementation stops at the first short read, which leaves the tail of the array holding stale content when the source
232+
* delivers data in chunks. Callers compare the whole array against a multi-character delimiter, so a partial fill makes them miss a delimiter that is
233+
* really there.
234+
* </p>
235+
*
236+
* @param array the buffer to fill.
237+
* @return the number of characters peeked, or {@link IOUtils#EOF} at the end of the stream.
238+
* @throws IOException If an I/O error occurs.
239+
*/
240+
@Override
241+
public int peek(final char[] array) throws IOException {
242+
final int length = array.length;
243+
if (length == 0) {
244+
return 0;
245+
}
246+
super.mark(length);
247+
int len = 0;
248+
while (len < length) {
249+
final int more = super.read(array, len, length - len);
250+
if (more == EOF) {
251+
break;
252+
}
253+
len += more;
254+
}
255+
super.reset();
256+
return len == 0 ? EOF : len;
257+
}
258+
228259
@Override
229260
public int read() throws IOException {
230261
final int current = super.read();
@@ -244,7 +275,16 @@ public int read(final char[] buf, final int offset, final int length) throws IOE
244275
if (length == 0) {
245276
return 0;
246277
}
247-
final int len = super.read(buf, offset, length);
278+
int len = super.read(buf, offset, length);
279+
// The underlying buffered reader stops early once the source reports it is not ready, so a stream that delivers data in chunks (a socket or a pipe)
280+
// yields a short read. Callers match multi-character sequences against this buffer, so keep reading until it is full or the source is exhausted.
281+
while (len > 0 && len < length) {
282+
final int more = super.read(buf, offset + len, length - len);
283+
if (more == EOF) {
284+
break;
285+
}
286+
len += more;
287+
}
248288
if (encoder != null && len > 0) {
249289
this.bytesRead += getEncodedCharLength(buf, offset, len);
250290
}

src/test/java/org/apache/commons/csv/CSVParserTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,6 +1709,19 @@ void testParseWithDelimiterStringWithEscape() throws IOException {
17091709
}
17101710
}
17111711

1712+
@Test
1713+
void testParseWithDelimiterStringFromChunkedReader() throws IOException {
1714+
final CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setDelimiter("[|]").setEscape('!').get();
1715+
try (CSVParser csvParser = csvFormat.parse(new ChunkedReader("a[|]b\r\nc![!|!]d[|]e"))) {
1716+
CSVRecord csvRecord = csvParser.nextRecord();
1717+
assertEquals("a", csvRecord.get(0));
1718+
assertEquals("b", csvRecord.get(1));
1719+
csvRecord = csvParser.nextRecord();
1720+
assertEquals("c[|]d", csvRecord.get(0));
1721+
assertEquals("e", csvRecord.get(1));
1722+
}
1723+
}
1724+
17121725
@Test
17131726
void testParseWithDelimiterStringWithQuote() throws IOException {
17141727
final String source = "'a[|]b[|]c'[|]xyz\r\nabc[abc][|]xyz";
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.commons.csv;
21+
22+
import java.io.IOException;
23+
import java.io.StringReader;
24+
25+
/**
26+
* A {@link StringReader} that hands out one character per call and never reports itself ready, like a socket or a pipe that delivers data in chunks.
27+
*/
28+
final class ChunkedReader extends StringReader {
29+
30+
ChunkedReader(final String content) {
31+
super(content);
32+
}
33+
34+
@Override
35+
public int read(final char[] buf, final int offset, final int length) throws IOException {
36+
return length <= 0 ? 0 : super.read(buf, offset, 1);
37+
}
38+
39+
@Override
40+
public boolean ready() {
41+
return false;
42+
}
43+
}

src/test/java/org/apache/commons/csv/ExtendedBufferedReaderTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,37 @@ void testReadLookahead2() throws Exception {
240240
assertEquals('d', br.getLastChar());
241241
}
242242
}
243+
244+
@Test
245+
void testReadAndPeekArrayFromChunkedReader() throws Exception {
246+
try (ExtendedBufferedReader br = new ExtendedBufferedReader(new ChunkedReader("abcdef"))) {
247+
final char[] peeked = new char[3];
248+
assertEquals(3, br.peek(peeked));
249+
assertArrayEquals(new char[] { 'a', 'b', 'c' }, peeked);
250+
final char[] read = new char[3];
251+
assertEquals(3, br.read(read, 0, 3));
252+
assertArrayEquals(new char[] { 'a', 'b', 'c' }, read);
253+
}
254+
}
255+
256+
@Test
257+
void testReadArrayPastEndOfChunkedReader() throws Exception {
258+
try (ExtendedBufferedReader br = new ExtendedBufferedReader(new ChunkedReader("ab"))) {
259+
final char[] read = new char[4];
260+
assertEquals(2, br.read(read, 0, 4));
261+
assertEquals('a', read[0]);
262+
assertEquals('b', read[1]);
263+
assertEquals(EOF, br.read(read, 0, 4));
264+
}
265+
}
266+
267+
@Test
268+
void testPeekArrayPastEndOfChunkedReader() throws Exception {
269+
try (ExtendedBufferedReader br = new ExtendedBufferedReader(new ChunkedReader("ab"))) {
270+
final char[] peeked = new char[4];
271+
assertEquals(2, br.peek(peeked));
272+
assertEquals('a', peeked[0]);
273+
assertEquals('b', peeked[1]);
274+
}
275+
}
243276
}

0 commit comments

Comments
 (0)