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