Skip to main content

rudb_csv/
scan.rs

1//! Bytes into fields.
2//!
3//! Two ways of doing it, which agree. [`record`] reads one record into owned strings, and it is
4//! what the sniffer runs: working out the delimiter means splitting the sample under each candidate
5//! and seeing which one gives every line the same number of fields, and that has to be the same
6//! splitting the reader will do or the sniffer answers a question nobody asked. [`records`] reads
7//! a chunk's worth of records at once and writes down where each field is rather than copying it,
8//! and it is what the reader runs. The tests at the bottom hold the two to the same answer on a
9//! few thousand generated files, which is what lets the sniffer keep the simple one.
10//!
11//! A record ends at a newline that is not inside a quoted field, and the three line endings are all
12//! accepted whatever the file mostly uses. DuckDB reports the one it found and reads either, and a
13//! file that a text editor has half converted is a real thing.
14//!
15//! [`records`] finds the delimiters, quotes and line endings sixty four bytes at a time, as one bit
16//! a byte in a `u64`, and works out which of them are inside a quoted field with a prefix XOR over
17//! the quote bits, the way simdjson and simdcsv do. A field only has to be looked at byte by byte
18//! when it has a quote in it, and then only to check that the quotes are where a quoted field puts
19//! them. A quote anywhere else, which is a literal character in the middle of a bare field, hands
20//! that one record to the byte loop, since the parity trick has no way to know the quote was not
21//! meant. A dialect whose escape is not its quote is read by the byte loop throughout.
22//!
23//! Fields used to arrive as owned strings, a copy per field before the conversion made a second
24//! one. The borrowed ranges were the change to make when there was a number saying it mattered, and
25//! the number is this: on a 200,000 row file shaped like TPC-H lineitem, 27 MB, one core of an
26//! Apple M4 read 95 MB/s a record at a time and 613 MB/s a chunk at a time, with the scan itself
27//! running at about 3 GB/s and the rest going to the column conversions. The ignored test
28//! `reads_lineitem_faster_a_chunk_at_a_time` in the reader measures it again.
29
30use std::borrow::Cow;
31
32use rudb_common::{Error, Result};
33
34use crate::dialect::Dialect;
35
36/// Reads the record starting at `from` into `out`, and answers where the next one starts.
37///
38/// `None` means the buffer does not hold a whole record: either it ran out mid line and more bytes
39/// may follow, or a quoted field was left open. With `eof` set there are no more bytes, so a last
40/// line with no newline on the end is a record like any other and only an open quote is short.
41///
42/// # Errors
43///
44/// When a quoted field has something other than a delimiter or a line ending after its closing
45/// quote, which is a file that is not the file it claims to be and reading past it would invent
46/// data.
47pub fn record(
48    bytes: &[u8],
49    from: usize,
50    dialect: Dialect,
51    eof: bool,
52    out: &mut Vec<String>,
53) -> Result<Option<usize>> {
54    if from >= bytes.len() {
55        // No bytes left is no record, and it is not a record of one empty field. The difference
56        // matters at the end of a file, where a reader that took the second reading would hand back
57        // an empty row forever, and it matters for the last line of a file that ends in a newline,
58        // which is not a row of nothing.
59        return Ok(None);
60    }
61    let quote = dialect.quote_byte();
62    let escape = dialect.escape_byte();
63    let mut at = from;
64    let mut count = 0;
65    let mut field = Vec::new();
66    loop {
67        field.clear();
68        if bytes.get(at) == Some(&quote) {
69            at += 1;
70            loop {
71                let Some(&byte) = bytes.get(at) else { return Ok(None) };
72                if byte == escape && bytes.get(at + 1) == Some(&quote) {
73                    field.push(quote);
74                    at += 2;
75                    continue;
76                }
77                if byte == quote {
78                    at += 1;
79                    break;
80                }
81                field.push(byte);
82                at += 1;
83            }
84            match bytes.get(at) {
85                None if !eof => return Ok(None),
86                None => {}
87                Some(&byte) if byte == dialect.delimiter || byte == b'\n' || byte == b'\r' => {}
88                Some(&byte) => return Err(after_quote(byte)),
89            }
90        } else {
91            while let Some(&byte) = bytes.get(at) {
92                if byte == dialect.delimiter || byte == b'\n' || byte == b'\r' {
93                    break;
94                }
95                field.push(byte);
96                at += 1;
97            }
98            if at >= bytes.len() && !eof {
99                return Ok(None);
100            }
101        }
102        place(out, count, &field);
103        count += 1;
104        match bytes.get(at) {
105            Some(&byte) if byte == dialect.delimiter => at += 1,
106            Some(b'\r') => {
107                at += 1;
108                if bytes.get(at) == Some(&b'\n') {
109                    at += 1;
110                } else if at >= bytes.len() && !eof {
111                    // A trailing carriage return may be the first half of a `\r\n` that has not
112                    // arrived, and guessing wrong here splits one record into two.
113                    return Ok(None);
114                }
115                break;
116            }
117            Some(b'\n') => {
118                at += 1;
119                break;
120            }
121            Some(_) => unreachable!("a field stops at a delimiter, a line ending or the end"),
122            None => break,
123        }
124    }
125    out.truncate(count);
126    Ok(Some(at))
127}
128
129/// Writes the field at `at`, reusing the string that is already there.
130///
131/// A CSV file is bytes and the encoding is not stated anywhere in it. UTF-8 comes through unchanged
132/// because no byte of a multi byte sequence can be a delimiter or a quote. A byte that is not valid
133/// UTF-8 becomes the replacement character rather than an error, which is what one bad byte in a
134/// text file deserves, and which is also what DuckDB does when it is not told to check.
135fn place(out: &mut Vec<String>, at: usize, field: &[u8]) {
136    let text = String::from_utf8_lossy(field);
137    match out.get_mut(at) {
138        Some(held) => {
139            held.clear();
140            held.push_str(&text);
141        }
142        None => out.push(text.into_owned()),
143    }
144}
145
146/// The error for a quoted field with something after its closing quote, shared by both readers so
147/// that they cannot come to say it differently.
148fn after_quote(byte: u8) -> Error {
149    Error::io(format!(
150        "a quoted value is followed by '{}' rather than by a delimiter or the end of the line",
151        byte as char
152    ))
153}
154
155/// Where one field's bytes are in the buffer it was read from.
156///
157/// A quoted field's range is what is between its quotes. `escaped` says a doubled quote or an
158/// escaped one is in there, so the bytes are not the value yet and [`Span::text`] has to take the
159/// escapes out, which is the one case that copies before the value is used.
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub struct Span {
162    /// The first byte of the field.
163    pub start: usize,
164    /// One past the last byte of the field.
165    pub end: usize,
166    /// Whether the bytes still hold escapes.
167    pub escaped: bool,
168}
169
170impl Span {
171    /// Whether the field is empty, which the reader turns into a null.
172    ///
173    /// A field with an escape in it is never empty once the escape is taken out, since every escape
174    /// leaves a quote behind, so the range answers for the value.
175    #[must_use]
176    pub const fn is_empty(self) -> bool {
177        self.start == self.end
178    }
179
180    /// The bytes of the field as they are in the buffer, escapes and all.
181    #[must_use]
182    pub fn raw(self, bytes: &[u8]) -> &[u8] {
183        &bytes[self.start..self.end]
184    }
185
186    /// The field as text, borrowed from the buffer unless it had an escape or a byte that is not
187    /// UTF-8 in it.
188    ///
189    /// The bytes that are not UTF-8 become the replacement character, exactly as
190    /// `String::from_utf8_lossy` makes them, because that is what [`record`] does and the two have
191    /// to agree.
192    #[must_use]
193    pub fn text(self, bytes: &[u8], dialect: Dialect) -> Cow<'_, str> {
194        let raw = self.raw(bytes);
195        if !self.escaped {
196            return String::from_utf8_lossy(raw);
197        }
198        Cow::Owned(String::from_utf8_lossy(&unescape(raw, dialect)).into_owned())
199    }
200}
201
202/// The bytes of a quoted field with its escapes taken out.
203///
204/// This pairs the bytes up exactly the way the loop that found the field did, from the left, an
205/// escape byte followed by a quote being one quote and anything else being itself. The range ends
206/// before the closing quote, so there is no closing quote in it to be mistaken for the second half
207/// of a pair, and reading it again from the same end gives the same pairs.
208fn unescape(raw: &[u8], dialect: Dialect) -> Vec<u8> {
209    let quote = dialect.quote_byte();
210    let escape = dialect.escape_byte();
211    let mut out = Vec::with_capacity(raw.len());
212    let mut at = 0;
213    while let Some(&byte) = raw.get(at) {
214        if byte == escape && raw.get(at + 1) == Some(&quote) {
215            out.push(quote);
216            at += 2;
217        } else {
218            out.push(byte);
219            at += 1;
220        }
221    }
222    out
223}
224
225/// A chunk's worth of records, as ranges into the buffer they were read from.
226///
227/// One list of fields for all of the records and one list of where each record's fields end, which
228/// is two allocations that are reused from chunk to chunk rather than one per row.
229#[derive(Debug, Clone, Default)]
230pub struct Records {
231    spans: Vec<Span>,
232    ends: Vec<usize>,
233}
234
235impl Records {
236    /// No records, with the room the last chunk needed kept.
237    pub fn clear(&mut self) {
238        self.spans.clear();
239        self.ends.clear();
240    }
241
242    /// How many whole records have been read.
243    #[must_use]
244    pub fn len(&self) -> usize {
245        self.ends.len()
246    }
247
248    /// Whether no whole record has been read.
249    #[must_use]
250    pub fn is_empty(&self) -> bool {
251        self.ends.is_empty()
252    }
253
254    /// The fields of record `at`, in order.
255    ///
256    /// # Panics
257    ///
258    /// If there is no record `at`.
259    #[must_use]
260    pub fn fields(&self, at: usize) -> &[Span] {
261        let start = if at == 0 { 0 } else { self.ends[at - 1] };
262        &self.spans[start..self.ends[at]]
263    }
264
265    /// Field `column` of record `row`, or `None` when the record is shorter than that.
266    #[must_use]
267    pub fn field(&self, row: usize, column: usize) -> Option<Span> {
268        let start = if row == 0 { 0 } else { self.ends[row - 1] };
269        let at = start + column;
270        if at < self.ends[row] { Some(self.spans[at]) } else { None }
271    }
272
273    /// Moves every range down by `by` bytes, for a buffer that has just had that many bytes taken
274    /// off its front.
275    pub fn shift(&mut self, by: usize) {
276        if by == 0 {
277            return;
278        }
279        for span in &mut self.spans {
280            span.start -= by;
281            span.end -= by;
282        }
283    }
284}
285
286/// Reads whole records starting at `from` into `out`, until it holds `limit` of them or the buffer
287/// has no whole record left, and answers where the first record it did not take starts.
288///
289/// The rules are [`record`]'s, including `eof`: without it a record the buffer ends in the middle
290/// of is left for when there are more bytes, and with it a last line with no newline is a record
291/// and only an open quote is short. Fewer than `limit` records and not at the end of the file means
292/// the caller should read more and ask again from the answer.
293///
294/// # Errors
295///
296/// The one [`record`] reports, for a quoted field with something after its closing quote. The
297/// records before it are in `out` and the one it was found in is not.
298pub fn records(
299    bytes: &[u8],
300    from: usize,
301    dialect: Dialect,
302    eof: bool,
303    limit: usize,
304    out: &mut Records,
305) -> Result<usize> {
306    let quote = dialect.quote_byte();
307    let delimiter = dialect.delimiter;
308    let structural = |byte: u8| byte == quote || byte == b'\n' || byte == b'\r';
309    if dialect.escape_byte() == quote && !structural(delimiter) {
310        blocks(bytes, from, dialect, eof, limit, out)
311    } else {
312        let mut at = from;
313        while out.len() < limit {
314            let Some(next) = spans(bytes, at, dialect, eof, &mut out.spans)? else { break };
315            out.ends.push(out.spans.len());
316            at = next;
317        }
318        Ok(at)
319    }
320}
321
322/// [`records`] for a dialect whose escape is its quote, sixty four bytes at a time.
323///
324/// Each block becomes two masks, the quotes and the bytes that can end a field. The prefix XOR of
325/// the quote mask has a bit set for every byte after an odd number of quotes, which is every byte
326/// inside a quoted field, and a doubled quote flips it twice between two adjacent bytes and so
327/// changes nothing. Whether the block ended inside a quote is carried into the next one. The field
328/// endings left once those bits are taken away are walked in order with `trailing_zeros`.
329///
330/// That is only right while every quote is where a quoted field puts it: first and last in its
331/// field, with any others in pairs. A field with no quote in it cannot be wrong, because the bits
332/// were outside a quote at its start and nothing in it flips them. A field with a quote in it is
333/// checked, and one that is not shaped like a quoted field goes to the byte loop from the start of
334/// its record, after which the blocks start again behind that record with the parity cleared.
335fn blocks(
336    bytes: &[u8],
337    from: usize,
338    dialect: Dialect,
339    eof: bool,
340    limit: usize,
341    out: &mut Records,
342) -> Result<usize> {
343    let delimiter = dialect.delimiter;
344    let quote = dialect.quote_byte();
345    let len = bytes.len();
346    let mut record = from;
347    loop {
348        // Where the fields of the record being read start in `out`, so that a record that turns
349        // out to be short or to need the byte loop can be taken back out.
350        let mut mark = out.spans.len();
351        let mut field = record;
352        let mut quoted = false;
353        let mut inside = 0u64;
354        let mut block = record;
355        'blocks: while block < len && out.len() < limit {
356            let end = len.min(block + 64);
357            let (quotes, ends) = masks(&bytes[block..end], delimiter, quote);
358            let prefix = prefix_xor(quotes) ^ inside;
359            inside = 0u64.wrapping_sub(prefix >> 63);
360            let mut structural = ends & !prefix;
361            while structural != 0 {
362                let bit = structural.trailing_zeros() as usize;
363                structural &= structural - 1;
364                let at = block + bit;
365                if at < field {
366                    // The `\n` of a `\r\n`, which the `\r` already dealt with.
367                    continue;
368                }
369                let low = field.saturating_sub(block);
370                if quoted || quotes & below(bit) & !below(low) != 0 {
371                    let Some(escaped) = enclosed(&bytes[field..at], quote) else {
372                        break 'blocks;
373                    };
374                    out.spans.push(Span { start: field + 1, end: at - 1, escaped });
375                    quoted = false;
376                } else {
377                    out.spans.push(Span { start: field, end: at, escaped: false });
378                }
379                let byte = bytes[at];
380                if byte == delimiter {
381                    field = at + 1;
382                    continue;
383                }
384                let mut next = at + 1;
385                if byte == b'\r' {
386                    match bytes.get(next) {
387                        Some(b'\n') => next += 1,
388                        Some(_) => {}
389                        None if eof => {}
390                        // The first half of a `\r\n` whose second half has not been read, the
391                        // same wait `record` makes.
392                        None => {
393                            out.spans.truncate(mark);
394                            return Ok(record);
395                        }
396                    }
397                }
398                out.ends.push(out.spans.len());
399                mark = out.spans.len();
400                record = next;
401                field = next;
402                if out.len() == limit {
403                    return Ok(record);
404                }
405            }
406            if field < end && quotes >> field.saturating_sub(block) != 0 {
407                quoted = true;
408            }
409            block = end;
410        }
411        if out.len() >= limit || record >= len {
412            return Ok(record);
413        }
414        // Either a field whose quotes the blocks cannot vouch for, or the end of the buffer in the
415        // middle of a record. The byte loop settles both, the second one being where a last line
416        // with no newline, a record that is not all here yet and an open quote are told apart.
417        out.spans.truncate(mark);
418        let Some(next) = spans(bytes, record, dialect, eof, &mut out.spans)? else {
419            return Ok(record);
420        };
421        out.ends.push(out.spans.len());
422        record = next;
423    }
424}
425
426/// The quotes in a block of up to sixty four bytes, and the delimiters and line endings, one bit a
427/// byte with the first byte in the lowest bit.
428///
429/// Written as a plain loop over a fixed size array so that the compiler turns it into vector
430/// compares, which it does, and without `unsafe` or a platform's intrinsics. A short block at the
431/// end of the buffer is padded and the bits for the padding are cleared afterwards, so the padding
432/// byte does not have to be one that cannot be a delimiter.
433#[inline]
434fn masks(block: &[u8], delimiter: u8, quote: u8) -> (u64, u64) {
435    let mut padded = [0u8; 64];
436    let full: &[u8; 64] = if let Ok(full) = block.try_into() {
437        full
438    } else {
439        padded[..block.len()].copy_from_slice(block);
440        &padded
441    };
442    // One byte a byte first, the quote in the low bit and a field ending in the next one, which is
443    // a loop with nothing between the lanes and becomes a handful of vector compares. Then eight
444    // of those bytes at a time are gathered into eight bits with one multiply.
445    let mut class = [0u8; 64];
446    for (class, &byte) in class.iter_mut().zip(full) {
447        let end = (byte == delimiter) | (byte == b'\n') | (byte == b'\r');
448        *class = u8::from(byte == quote) | (u8::from(end) << 1);
449    }
450    let mut quotes = 0u64;
451    let mut ends = 0u64;
452    for (at, eight) in class.chunks_exact(8).enumerate() {
453        let word = u64::from_le_bytes(eight.try_into().expect("eight bytes"));
454        quotes |= gather(word & LOW_BITS) << (8 * at);
455        ends |= gather((word >> 1) & LOW_BITS) << (8 * at);
456    }
457    if block.len() < 64 {
458        let live = below(block.len());
459        quotes &= live;
460        ends &= live;
461    }
462    (quotes, ends)
463}
464
465/// The lowest bit of each of the eight bytes of a word.
466const LOW_BITS: u64 = 0x0101_0101_0101_0101;
467
468/// Eight bytes that are each nought or one as eight bits, the first byte in the lowest bit.
469///
470/// The multiply puts a copy of byte `i` at bit `56 + i` for every `i` at once, and every other
471/// copy it makes lands on a bit of its own below bit 56, so nothing carries into the top byte.
472const fn gather(word: u64) -> u64 {
473    word.wrapping_mul(0x0102_0408_1020_4080) >> 56
474}
475
476/// Every bit set that has an odd number of set bits at or below it in `bits`.
477const fn prefix_xor(mut bits: u64) -> u64 {
478    bits ^= bits << 1;
479    bits ^= bits << 2;
480    bits ^= bits << 4;
481    bits ^= bits << 8;
482    bits ^= bits << 16;
483    bits ^= bits << 32;
484    bits
485}
486
487/// The bits below bit `count`, for a count under sixty four.
488const fn below(count: usize) -> u64 {
489    (1u64 << count) - 1
490}
491
492/// Whether a field is shaped like a quoted field, and if it is, whether it has an escape in it.
493///
494/// Shaped means a quote first and last and every quote between them one of a doubled pair, which is
495/// exactly the field the byte loop would read as quoted and close on the last byte. Pairs are taken
496/// from the left the way the byte loop takes them, so `"a"""` is `a"` and `"a""` is not a field the
497/// byte loop would have closed where it stops.
498fn enclosed(field: &[u8], quote: u8) -> Option<bool> {
499    let [first, inner @ .., last] = field else { return None };
500    if *first != quote || *last != quote {
501        return None;
502    }
503    let mut escaped = false;
504    let mut at = 0;
505    while let Some(found) = inner[at..].iter().position(|&byte| byte == quote) {
506        if inner.get(at + found + 1) != Some(&quote) {
507            return None;
508        }
509        escaped = true;
510        at += found + 2;
511    }
512    Some(escaped)
513}
514
515/// [`record`] writing ranges rather than strings, which is the byte loop [`records`] falls back on.
516///
517/// The same loop step for step, with the pushes of bytes taken out and a range recorded in their
518/// place. On `None` or an error nothing this call added is left in `out`.
519fn spans(
520    bytes: &[u8],
521    from: usize,
522    dialect: Dialect,
523    eof: bool,
524    out: &mut Vec<Span>,
525) -> Result<Option<usize>> {
526    if from >= bytes.len() {
527        return Ok(None);
528    }
529    let mark = out.len();
530    let quote = dialect.quote_byte();
531    let escape = dialect.escape_byte();
532    let mut at = from;
533    loop {
534        let span;
535        if bytes.get(at) == Some(&quote) {
536            at += 1;
537            let start = at;
538            let mut escaped = false;
539            loop {
540                let Some(&byte) = bytes.get(at) else {
541                    out.truncate(mark);
542                    return Ok(None);
543                };
544                if byte == escape && bytes.get(at + 1) == Some(&quote) {
545                    escaped = true;
546                    at += 2;
547                    continue;
548                }
549                if byte == quote {
550                    break;
551                }
552                at += 1;
553            }
554            span = Span { start, end: at, escaped };
555            at += 1;
556            match bytes.get(at) {
557                None if !eof => {
558                    out.truncate(mark);
559                    return Ok(None);
560                }
561                None => {}
562                Some(&byte) if byte == dialect.delimiter || byte == b'\n' || byte == b'\r' => {}
563                Some(&byte) => {
564                    out.truncate(mark);
565                    return Err(after_quote(byte));
566                }
567            }
568        } else {
569            let start = at;
570            while let Some(&byte) = bytes.get(at) {
571                if byte == dialect.delimiter || byte == b'\n' || byte == b'\r' {
572                    break;
573                }
574                at += 1;
575            }
576            if at >= bytes.len() && !eof {
577                out.truncate(mark);
578                return Ok(None);
579            }
580            span = Span { start, end: at, escaped: false };
581        }
582        out.push(span);
583        match bytes.get(at) {
584            Some(&byte) if byte == dialect.delimiter => at += 1,
585            Some(b'\r') => {
586                at += 1;
587                if bytes.get(at) == Some(&b'\n') {
588                    at += 1;
589                } else if at >= bytes.len() && !eof {
590                    out.truncate(mark);
591                    return Ok(None);
592                }
593                break;
594            }
595            Some(b'\n') => {
596                at += 1;
597                break;
598            }
599            Some(_) => unreachable!("a field stops at a delimiter, a line ending or the end"),
600            None => break,
601        }
602    }
603    Ok(Some(at))
604}
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609
610    fn split(bytes: &[u8], dialect: Dialect) -> Vec<Vec<String>> {
611        let mut rows = Vec::new();
612        let mut fields = Vec::new();
613        let mut at = 0;
614        while at < bytes.len() {
615            let next = record(bytes, at, dialect, true, &mut fields)
616                .expect("splits")
617                .expect("a whole record");
618            rows.push(fields.clone());
619            at = next;
620        }
621        rows
622    }
623
624    fn comma() -> Dialect {
625        Dialect { delimiter: b',', quote: Some(b'"'), escape: Some(b'"'), header: true }
626    }
627
628    #[test]
629    fn a_line_of_fields_is_the_fields_of_that_line() {
630        assert_eq!(split(b"a,b,c\n1,2,3\n", comma()), [["a", "b", "c"], ["1", "2", "3"]]);
631    }
632
633    #[test]
634    fn the_last_line_does_not_need_a_newline_on_it() {
635        assert_eq!(split(b"a,b\n1,2", comma()), [["a", "b"], ["1", "2"]]);
636    }
637
638    #[test]
639    fn all_three_line_endings_end_a_line() {
640        assert_eq!(split(b"a\r\nb\rc\n", comma()), [["a"], ["b"], ["c"]]);
641    }
642
643    #[test]
644    fn a_quoted_field_may_hold_the_delimiter_and_a_newline() {
645        assert_eq!(split(b"1,\"x,y\"\n", comma()), [["1", "x,y"]]);
646        assert_eq!(split(b"1,\"x\ny\"\n", comma()), [["1", "x\ny"]]);
647    }
648
649    #[test]
650    fn a_doubled_quote_inside_a_quoted_field_is_one_quote() {
651        assert_eq!(split(b"1,\"say \"\"hi\"\"\"\n", comma()), [["1", "say \"hi\""]]);
652    }
653
654    #[test]
655    fn an_empty_field_is_an_empty_string_here_and_becomes_a_null_above() {
656        assert_eq!(split(b"1,,3\n", comma()), [["1", "", "3"]]);
657        assert_eq!(split(b"1,\"\",3\n", comma()), [["1", "", "3"]]);
658    }
659
660    #[test]
661    fn a_trailing_delimiter_makes_a_last_empty_field() {
662        assert_eq!(split(b"1|x|\n", Dialect { delimiter: b'|', ..comma() }), [["1", "x", ""]]);
663    }
664
665    #[test]
666    fn a_quote_in_the_middle_of_a_bare_field_is_just_a_character() {
667        assert_eq!(split(b"1,he said \"hi\"\n", comma()), [["1", "he said \"hi\""]]);
668    }
669
670    #[test]
671    fn a_record_that_the_buffer_does_not_hold_all_of_is_not_a_record_yet() {
672        let mut fields = Vec::new();
673        assert_eq!(record(b"a,b", 0, comma(), false, &mut fields).unwrap(), None);
674        assert_eq!(record(b"a,\"b", 0, comma(), true, &mut fields).unwrap(), None);
675        assert_eq!(record(b"a,b\n", 0, comma(), false, &mut fields).unwrap(), Some(4));
676    }
677
678    #[test]
679    fn rubbish_after_a_closing_quote_is_an_error_rather_than_a_guess() {
680        let mut fields = Vec::new();
681        let error = record(b"\"x\"y,2\n", 0, comma(), true, &mut fields).unwrap_err();
682        assert!(error.message().contains("quoted value"), "{error}");
683    }
684
685    #[test]
686    fn utf8_survives_being_read_one_byte_at_a_time() {
687        assert_eq!(split("a,héllo\n".as_bytes(), comma()), [["a", "héllo"]]);
688    }
689}
690
691/// The two readers held to the same answers.
692///
693/// [`record`] is the reference, because it is the loop the sniffer runs and the one every rule in
694/// this file was first written down in. Everything [`records`] reports is compared with it: the
695/// fields of every record, where the reading stopped, and the error if there was one.
696#[cfg(test)]
697mod agree {
698    use super::*;
699
700    /// What a whole scan found: the records, where it stopped, and the error it stopped on.
701    type Outcome = (Vec<Vec<String>>, usize, Option<String>);
702
703    fn by_record(bytes: &[u8], from: usize, dialect: Dialect, eof: bool) -> Outcome {
704        let mut rows = Vec::new();
705        let mut fields = Vec::new();
706        let mut at = from;
707        loop {
708            match record(bytes, at, dialect, eof, &mut fields) {
709                Ok(Some(next)) => {
710                    rows.push(fields.clone());
711                    at = next;
712                }
713                Ok(None) => return (rows, at, None),
714                Err(error) => return (rows, at, Some(error.to_string())),
715            }
716        }
717    }
718
719    fn by_chunk(bytes: &[u8], from: usize, dialect: Dialect, eof: bool, limit: usize) -> Outcome {
720        let mut rows = Vec::new();
721        let mut at = from;
722        let mut out = Records::default();
723        loop {
724            out.clear();
725            let result = records(bytes, at, dialect, eof, limit, &mut out);
726            for row in 0..out.len() {
727                rows.push(
728                    out.fields(row)
729                        .iter()
730                        .map(|span| span.text(bytes, dialect).into_owned())
731                        .collect(),
732                );
733            }
734            match result {
735                Ok(next) => {
736                    at = next;
737                    if out.len() < limit {
738                        return (rows, at, None);
739                    }
740                }
741                Err(error) => {
742                    // The error leaves the records before it in `out`, so the position is the
743                    // start of the record it was found in, which is where `record` stopped too.
744                    let mut from = at;
745                    let mut fields = Vec::new();
746                    for _ in 0..out.len() {
747                        from = record(bytes, from, dialect, eof, &mut fields).unwrap().unwrap();
748                    }
749                    return (rows, from, Some(error.to_string()));
750                }
751            }
752        }
753    }
754
755    /// Both readers over `bytes`, whole and cut at every one of `cuts` the way a refill cuts it,
756    /// with a few chunk sizes.
757    fn check(bytes: &[u8], dialect: Dialect, cuts: &[usize]) {
758        for eof in [true, false] {
759            let expected = by_record(bytes, 0, dialect, eof);
760            for limit in [1, 2, 3, 7, 8192] {
761                assert_eq!(
762                    by_chunk(bytes, 0, dialect, eof, limit),
763                    expected,
764                    "{:?} under {dialect:?}, eof {eof}, {limit} at a time",
765                    String::from_utf8_lossy(bytes),
766                );
767            }
768        }
769        let whole = by_record(bytes, 0, dialect, true);
770        for &cut in cuts {
771            let cut = cut.min(bytes.len());
772            // The front of the file as a buffer that more bytes will follow, and then the rest of
773            // it from wherever that stopped, which is what a refill does.
774            let (mut rows, at, error) = by_chunk(&bytes[..cut], 0, dialect, false, 8192);
775            if error.is_some() {
776                assert_eq!(error, whole.2, "an error in the front is the error in the whole");
777                continue;
778            }
779            let (rest, end, error) = by_chunk(bytes, at, dialect, true, 8192);
780            rows.extend(rest);
781            assert_eq!(
782                (rows, end, error),
783                whole,
784                "{:?} under {dialect:?} cut at {cut}",
785                String::from_utf8_lossy(bytes),
786            );
787        }
788    }
789
790    fn dialects() -> [Dialect; 5] {
791        let comma =
792            Dialect { delimiter: b',', quote: Some(b'"'), escape: Some(b'"'), header: false };
793        [
794            comma,
795            Dialect { quote: None, escape: None, ..comma },
796            Dialect { delimiter: b'|', ..comma },
797            Dialect { delimiter: b'\t', quote: Some(b'\''), escape: Some(b'\''), ..comma },
798            Dialect { escape: Some(b'\\'), ..comma },
799        ]
800    }
801
802    #[test]
803    fn the_tricky_ones_split_the_same_both_ways() {
804        let long = "x".repeat(61);
805        let cases: Vec<Vec<u8>> = vec![
806            b"".to_vec(),
807            b"\n".to_vec(),
808            b"\n\n".to_vec(),
809            b"a".to_vec(),
810            b"a,b,c\n1,2,3\n".to_vec(),
811            b"a,b\n1,2".to_vec(),
812            b"a\r\nb\rc\n".to_vec(),
813            b"a\r".to_vec(),
814            b"a\r\r\n\n".to_vec(),
815            b"1,\"x,y\"\n2,\"x\ny\"\n".to_vec(),
816            b"1,\"say \"\"hi\"\"\"\n".to_vec(),
817            b"1,,3\n1,\"\",3\n".to_vec(),
818            b"1|x|\n1,x,\n".to_vec(),
819            b"1,he said \"hi\"\n2,x\n".to_vec(),
820            b"1,he said \"hi\n2,x\n".to_vec(),
821            b"\"x\"y,2\n".to_vec(),
822            b"\"x\"\"\n".to_vec(),
823            b"\"x\"\"".to_vec(),
824            b"a,\"b".to_vec(),
825            b"a,\"b\"".to_vec(),
826            b"\"".to_vec(),
827            b"\"\"\"\"\n".to_vec(),
828            b"\"a\"\"\",b\n".to_vec(),
829            b"\"a\\\"b\",c\n\"a\\\\\"\n".to_vec(),
830            b"'a,b'\t'c''d'\n".to_vec(),
831            b"x\xffy,\"\xfe\"\"\"\n".to_vec(),
832            "h\u{e9}llo,w\u{f6}rld\n".as_bytes().to_vec(),
833            format!("{long},\"a\nb\",c\n{long}\r\n\"{long}\"\"{long}\",d\n").into_bytes(),
834            format!("{long}ab\r\n{long}abc\r\n").into_bytes(),
835            format!("\"{long}\"\"\",\"\n\"\n{long},x\"\n").into_bytes(),
836        ];
837        for bytes in &cases {
838            let cuts: Vec<usize> = (0..=bytes.len()).collect();
839            for dialect in dialects() {
840                check(bytes, dialect, &cuts);
841            }
842        }
843    }
844
845    /// A small generator, since the crate has no dependencies to take one from.
846    struct Rng(u64);
847
848    impl Rng {
849        fn next(&mut self) -> u64 {
850            self.0 ^= self.0 << 13;
851            self.0 ^= self.0 >> 7;
852            self.0 ^= self.0 << 17;
853            self.0
854        }
855
856        fn below(&mut self, n: usize) -> usize {
857            (self.next() % n as u64) as usize
858        }
859    }
860
861    /// A file that is mostly a CSV file, with the kinds of damage real ones have.
862    fn generate(rng: &mut Rng) -> Vec<u8> {
863        const PIECES: [&[u8]; 20] = [
864            b"a",
865            b"1",
866            b"-2.5",
867            b"xyz",
868            b",",
869            b"|",
870            b"\t",
871            b"\"",
872            b"\"\"",
873            b"'",
874            b"\\",
875            b"\n",
876            b"\r",
877            b"\r\n",
878            b" ",
879            b"\xc3\xa9",
880            b"\xff",
881            b"2020-01-02",
882            b"",
883            b"0123456789abcdef",
884        ];
885        let mut out = Vec::new();
886        let rows = rng.below(40);
887        for _ in 0..rows {
888            let fields = 1 + rng.below(5);
889            for field in 0..fields {
890                if field > 0 {
891                    out.push(b",,,|\t"[rng.below(5)]);
892                }
893                let mut body = Vec::new();
894                for _ in 0..rng.below(6) {
895                    let piece = PIECES[rng.below(PIECES.len())];
896                    // Long runs of plain bytes now and then, so that fields straddle the blocks.
897                    if rng.below(10) == 0 {
898                        body.extend(std::iter::repeat_n(b'q', rng.below(90)));
899                    }
900                    body.extend_from_slice(piece);
901                }
902                match rng.below(4) {
903                    0 => {
904                        out.push(b'"');
905                        for &byte in &body {
906                            if byte == b'"' {
907                                out.push(b'"');
908                            }
909                            out.push(byte);
910                        }
911                        out.push(b'"');
912                    }
913                    1 => {
914                        out.push(b'"');
915                        out.extend_from_slice(&body);
916                        out.push(b'"');
917                    }
918                    _ => out.extend_from_slice(&body),
919                }
920            }
921            out.extend_from_slice([&b"\n"[..], b"\r\n", b"\r"][rng.below(3)]);
922        }
923        if rng.below(3) == 0 {
924            out.truncate(out.len().saturating_sub(1 + rng.below(3)));
925        }
926        out
927    }
928
929    #[test]
930    fn thousands_of_generated_files_split_the_same_both_ways() {
931        let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
932        for _ in 0..3000 {
933            let bytes = generate(&mut rng);
934            let cuts: Vec<usize> = (0..4).map(|_| rng.below(bytes.len() + 1)).collect();
935            for dialect in dialects() {
936                check(&bytes, dialect, &cuts);
937            }
938        }
939    }
940}