Skip to content

Commit 0819c4f

Browse files
committed
fix: peekLine left buffer in inconsistent state after compaction
peekLine captured csvBuffer.pos at entry and restored that absolute value at exit. fetchData, however, can compact the buffer mid-scan (shifting begin to 0 and adjusting pos). The restored absolute savedPos then pointed past the actual line start in the new layout. Subsequent skipLine(line.length()) advanced from the wrong base, eventually pushing pos out of buffer bounds and throwing StringIndexOutOfBoundsException - or, more dangerously, silently skipping records. Restore csvBuffer.pos to csvBuffer.begin instead. The invariant at peekLine entry is pos == begin (the caller just consumed previous records via skipLine), so this is equivalent in the no-compact case, and begin is itself shifted by compaction so it stays correct across a fetchData compact. JMH: FastCsvReadBenchmark unchanged within noise.
1 parent a6d3de5 commit 0819c4f

2 files changed

Lines changed: 18 additions & 3 deletions

File tree

lib/src/intTest/java/blackbox/reader/AbstractSkipLinesTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,23 @@ void peekLineSeesContentBeyondInternalBuffer() {
248248
.isStartingLineNumber(3).fields().containsExactly("value1", "value2"));
249249
}
250250

251+
@Test
252+
void peekLineSurvivesBufferCompaction() {
253+
// Force buffer compaction during peekLine's scan: skip a long first line
254+
// (so begin advances past the internal readSize), then peek a second long line
255+
// (so the scan exhausts the buffered tail and triggers compact-then-fetch).
256+
// A bug that captures `pos` as an absolute index and restores it post-compact
257+
// would leave the parser positioned mid-record afterwards.
258+
final int chunk = 8500;
259+
final String data = "S".repeat(chunk) + "\n" + "P".repeat(chunk) + "\nlast";
260+
261+
final CsvReader<CsvRecord> csv = crb.ofCsvRecord(new StringReader(data));
262+
263+
// Predicate matches the 3rd line. Forces peekLine on the 2nd (compaction-triggering)
264+
// line to be skipped past, exercising the post-peek pos/begin invariants.
265+
assertThat(csv.skipLines("last"::equals, 5)).isEqualTo(2);
266+
}
267+
251268
// Probe lengths around the 8 KiB lookahead boundary (8190..8194) with both LF and CRLF.
252269
// A predicate that matches only on the *exact* line content fails if peekLine
253270
// truncates at the boundary or splits CR/LF across a refill.

lib/src/main/java/de/siegmar/fastcsv/reader/StrictCsvParser.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,6 @@ public String peekLine() throws IOException {
358358
throw new EOFException();
359359
}
360360

361-
final int savedPos = csvBuffer.pos;
362-
363361
for (; csvBuffer.pos < csvBuffer.len || csvBuffer.fetchData(); csvBuffer.pos++) {
364362
final char c = csvBuffer.buf[csvBuffer.pos];
365363
if (c == CR || c == LF) {
@@ -368,7 +366,7 @@ public String peekLine() throws IOException {
368366
}
369367

370368
final String s = new String(csvBuffer.buf, csvBuffer.begin, csvBuffer.pos - csvBuffer.begin);
371-
csvBuffer.pos = savedPos;
369+
csvBuffer.pos = csvBuffer.begin;
372370
return s;
373371
}
374372

0 commit comments

Comments
 (0)