From 8fb49534345f28668be024f494abcdd68f6b6964 Mon Sep 17 00:00:00 2001 From: maxtaran2010 Date: Fri, 10 Jul 2026 00:27:08 +0300 Subject: [PATCH] csv-core: fix NFA read_record resumption after OutputFull When read_record_nfa was resumed after returning OutputFull (i.e. called a second time for the same record because the output buffer was full), it initialised the local nout counter to self.output_pos instead of 0. This caused two distinct bugs: 1. Field bytes were written at the wrong offset in the output slice. The caller passes &mut output[outlen..], so the first writable byte is always index 0. Starting nout at output_pos made the NFA write at output[output_pos], which is output_pos bytes too far into the buffer and can cause an out-of-bounds write or data corruption. 2. The returned nout was inflated by output_pos. The caller adds the returned nout to its own outlen accumulator, so the next call would pass a slice starting outlen too far along, corrupting all subsequent field data. The DFA implementation (read_record_dfa) handles this correctly: it starts nout at 0, computes field-end positions as self.output_pos + nout, and updates output_pos as self.output_pos + nout. This commit makes the NFA follow the same pattern. A regression test is added that forces two read_record calls for a single record by supplying a 3-byte output buffer for a 5-byte first field, and then verifies that both the written bytes and the field-end positions are correct after the resume. Co-Authored-By: Claude Sonnet 4.6 --- csv-core/src/reader.rs | 52 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/csv-core/src/reader.rs b/csv-core/src/reader.rs index 5bcbdfa..4065cae 100644 --- a/csv-core/src/reader.rs +++ b/csv-core/src/reader.rs @@ -877,7 +877,7 @@ impl Reader { if ends.is_empty() { return (ReadRecordResult::OutputEndsFull, 0, 0, 0); } - let (mut nin, mut nout, mut nend) = (0, self.output_pos, 0); + let (mut nin, mut nout, mut nend) = (0, 0, 0); let mut state = self.nfa_state; while nin < input.len() && nout < output.len() && nend < ends.len() { let (s, io) = self.transition_nfa(state, input[nin]); @@ -894,7 +894,7 @@ impl Reader { } state = s; if state.is_field_final() { - ends[nend] = nout; + ends[nend] = self.output_pos + nout; nend += 1; if state != NfaState::EndFieldDelim { break; @@ -908,7 +908,7 @@ impl Reader { nend >= ends.len(), ); self.nfa_state = state; - self.output_pos = if res.is_record() { 0 } else { nout }; + self.output_pos = if res.is_record() { 0 } else { self.output_pos + nout }; (res, nin, nout, nend) } @@ -2020,4 +2020,50 @@ mod tests { assert_eq!(result, Record); assert_eq!(ends[0], 3); } + + // Test that the NFA correctly handles resuming after OutputFull. + // + // Previously, read_record_nfa initialised `nout` to `self.output_pos` + // instead of 0, which caused two bugs when a record spanned multiple + // read_record calls (i.e. when OutputFull is returned): + // 1. Bytes were written at the wrong offset within the output buffer + // (`output[output_pos]` instead of `output[0]`). + // 2. The returned `nout` was inflated by `output_pos`, making the + // caller advance its accumulation pointer too far. + #[test] + fn nfa_output_full_resume() { + use crate::ReadRecordResult::*; + + // Use a record "hello,world\n" whose first field ("hello") is 5 + // bytes. We give the NFA only 3 bytes of output on the first call + // so it must return OutputFull and then be resumed. + let input = b("hello,world\n"); + let ends = &mut [0usize; 10]; + + let mut rdr = ReaderBuilder::new().nfa(true).build(); + + // First call: 3-byte output buffer → OutputFull after writing "hel". + let mut out1 = [0u8; 3]; + let (res1, nin1, nout1, nend1) = + rdr.read_record(input, &mut out1, ends); + assert_eq!(res1, OutputFull, "expected OutputFull on first call"); + assert_eq!(nout1, 3, "expected 3 bytes written"); + assert_eq!(&out1[..nout1], b"hel", "wrong bytes in first chunk"); + assert_eq!(nend1, 0, "no fields complete yet"); + + // Second call: resume with the remaining input and a fresh output + // buffer. The NFA must write "lo" then "world" (field data only, + // delimiters/terminators are discarded) and return Record. + let mut out2 = [0u8; 64]; + let (res2, _nin2, nout2, nend2) = + rdr.read_record(&input[nin1..], &mut out2, ends); + assert_eq!(res2, Record, "expected Record on second call"); + // out2 should contain "lo" + "world" = "loworld" (7 bytes, no delimiter) + assert_eq!(&out2[..nout2], b"loworld", "wrong bytes in second chunk"); + assert_eq!(nend2, 2, "expected 2 fields"); + // Field 0 ("hello") spans bytes 0..5 of the accumulated output. + assert_eq!(ends[0], 5, "field 0 should end at accumulated byte 5"); + // Field 1 ("world") spans bytes 5..10 of the accumulated output. + assert_eq!(ends[1], 10, "field 1 should end at accumulated byte 10"); + } }