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 two masks, the quotes and the bytes that can end a field. The prefix XOR of
371/// the quote mask has a bit set for every byte after an odd number of quotes, which is every byte
372/// inside a quoted field, and a doubled quote flips it twice between two adjacent bytes and so
373/// 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) = 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                let byte = bytes[at];
426                if byte == delimiter {
427                    field = at + 1;
428                    continue;
429                }
430                let mut next = at + 1;
431                if byte == b'\r' {
432                    match bytes.get(next) {
433                        Some(b'\n') => next += 1,
434                        Some(_) => {}
435                        None if eof => {}
436                        // The first half of a `\r\n` whose second half has not been read, the
437                        // same wait `record` makes.
438                        None => {
439                            out.spans.truncate(mark);
440                            return Ok(record);
441                        }
442                    }
443                }
444                out.ends.push(out.spans.len());
445                mark = out.spans.len();
446                record = next;
447                field = next;
448                if out.len() == limit {
449                    return Ok(record);
450                }
451            }
452            if field < end && quotes >> field.saturating_sub(block) != 0 {
453                quoted = true;
454            }
455            block = end;
456        }
457        if out.len() >= limit || record >= len {
458            return Ok(record);
459        }
460        // Either a field whose quotes the blocks cannot vouch for, or the end of the buffer in the
461        // middle of a record. The byte loop settles both, the second one being where a last line
462        // with no newline, a record that is not all here yet and an open quote are told apart.
463        out.spans.truncate(mark);
464        let Some(next) = spans(bytes, record, dialect, eof, &mut out.spans)? else {
465            return Ok(record);
466        };
467        out.ends.push(out.spans.len());
468        record = next;
469    }
470}
471
472/// The quotes in a block of up to sixty four bytes, and the delimiters and line endings, one bit a
473/// byte with the first byte in the lowest bit.
474///
475/// Written as a plain loop over a fixed size array so that the compiler turns it into vector
476/// compares, which it does, and without `unsafe` or a platform's intrinsics. A short block at the
477/// end of the buffer is padded and the bits for the padding are cleared afterwards, so the padding
478/// byte does not have to be one that cannot be a delimiter.
479#[inline]
480fn masks(block: &[u8], delimiter: u8, quote: u8) -> (u64, u64) {
481    let mut padded = [0u8; 64];
482    let full: &[u8; 64] = if let Ok(full) = block.try_into() {
483        full
484    } else {
485        padded[..block.len()].copy_from_slice(block);
486        &padded
487    };
488    // One byte a byte first, the quote in the low bit and a field ending in the next one, which is
489    // a loop with nothing between the lanes and becomes a handful of vector compares. Then eight
490    // of those bytes at a time are gathered into eight bits with one multiply.
491    let mut class = [0u8; 64];
492    for (class, &byte) in class.iter_mut().zip(full) {
493        let end = (byte == delimiter) | (byte == b'\n') | (byte == b'\r');
494        *class = u8::from(byte == quote) | (u8::from(end) << 1);
495    }
496    let mut quotes = 0u64;
497    let mut ends = 0u64;
498    for (at, eight) in class.chunks_exact(8).enumerate() {
499        let word = u64::from_le_bytes(eight.try_into().expect("eight bytes"));
500        quotes |= gather(word & LOW_BITS) << (8 * at);
501        ends |= gather((word >> 1) & LOW_BITS) << (8 * at);
502    }
503    if block.len() < 64 {
504        let live = below(block.len());
505        quotes &= live;
506        ends &= live;
507    }
508    (quotes, ends)
509}
510
511/// The lowest bit of each of the eight bytes of a word.
512const LOW_BITS: u64 = 0x0101_0101_0101_0101;
513
514/// Eight bytes that are each nought or one as eight bits, the first byte in the lowest bit.
515///
516/// The multiply puts a copy of byte `i` at bit `56 + i` for every `i` at once, and every other
517/// copy it makes lands on a bit of its own below bit 56, so nothing carries into the top byte.
518const fn gather(word: u64) -> u64 {
519    word.wrapping_mul(0x0102_0408_1020_4080) >> 56
520}
521
522/// Every bit set that has an odd number of set bits at or below it in `bits`.
523const fn prefix_xor(mut bits: u64) -> u64 {
524    bits ^= bits << 1;
525    bits ^= bits << 2;
526    bits ^= bits << 4;
527    bits ^= bits << 8;
528    bits ^= bits << 16;
529    bits ^= bits << 32;
530    bits
531}
532
533/// The bits below bit `count`, for a count under sixty four.
534const fn below(count: usize) -> u64 {
535    (1u64 << count) - 1
536}
537
538/// Whether a field is shaped like a quoted field, and if it is, whether it has an escape in it.
539///
540/// Shaped means a quote first and last and every quote between them one of a doubled pair, which is
541/// exactly the field the byte loop would read as quoted and close on the last byte. Pairs are taken
542/// from the left the way the byte loop takes them, so `"a"""` is `a"` and `"a""` is not a field the
543/// byte loop would have closed where it stops.
544fn enclosed(field: &[u8], quote: u8) -> Option<bool> {
545    let [first, inner @ .., last] = field else { return None };
546    if *first != quote || *last != quote {
547        return None;
548    }
549    let mut escaped = false;
550    let mut at = 0;
551    while let Some(found) = inner[at..].iter().position(|&byte| byte == quote) {
552        if inner.get(at + found + 1) != Some(&quote) {
553            return None;
554        }
555        escaped = true;
556        at += found + 2;
557    }
558    Some(escaped)
559}
560
561/// [`record`] writing ranges rather than strings, which is the byte loop [`records`] falls back on.
562///
563/// The same loop step for step, with the pushes of bytes taken out and a range recorded in their
564/// place. On `None` or an error nothing this call added is left in `out`.
565fn spans(
566    bytes: &[u8],
567    from: usize,
568    dialect: Dialect,
569    eof: bool,
570    out: &mut Vec<Span>,
571) -> Result<Option<usize>> {
572    if from >= bytes.len() {
573        return Ok(None);
574    }
575    let mark = out.len();
576    let quote = dialect.quote_byte();
577    let escape = dialect.escape_byte();
578    let mut at = from;
579    loop {
580        let span;
581        if bytes.get(at) == Some(&quote) {
582            at += 1;
583            let start = at;
584            let mut escaped = false;
585            loop {
586                let Some(&byte) = bytes.get(at) else {
587                    out.truncate(mark);
588                    return Ok(None);
589                };
590                if byte == escape && bytes.get(at + 1) == Some(&quote) {
591                    escaped = true;
592                    at += 2;
593                    continue;
594                }
595                if byte == quote {
596                    break;
597                }
598                at += 1;
599            }
600            span = Span::new(start, at, escaped);
601            at += 1;
602            match bytes.get(at) {
603                None if !eof => {
604                    out.truncate(mark);
605                    return Ok(None);
606                }
607                None => {}
608                Some(&byte) if byte == dialect.delimiter || byte == b'\n' || byte == b'\r' => {}
609                Some(&byte) => {
610                    out.truncate(mark);
611                    return Err(after_quote(byte));
612                }
613            }
614        } else {
615            let start = at;
616            while let Some(&byte) = bytes.get(at) {
617                if byte == dialect.delimiter || byte == b'\n' || byte == b'\r' {
618                    break;
619                }
620                at += 1;
621            }
622            if at >= bytes.len() && !eof {
623                out.truncate(mark);
624                return Ok(None);
625            }
626            span = Span::new(start, at, false);
627        }
628        out.push(span);
629        match bytes.get(at) {
630            Some(&byte) if byte == dialect.delimiter => at += 1,
631            Some(b'\r') => {
632                at += 1;
633                if bytes.get(at) == Some(&b'\n') {
634                    at += 1;
635                } else if at >= bytes.len() && !eof {
636                    out.truncate(mark);
637                    return Ok(None);
638                }
639                break;
640            }
641            Some(b'\n') => {
642                at += 1;
643                break;
644            }
645            Some(_) => unreachable!("a field stops at a delimiter, a line ending or the end"),
646            None => break,
647        }
648    }
649    Ok(Some(at))
650}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655
656    fn split(bytes: &[u8], dialect: Dialect) -> Vec<Vec<String>> {
657        let mut rows = Vec::new();
658        let mut fields = Vec::new();
659        let mut at = 0;
660        while at < bytes.len() {
661            let next = record(bytes, at, dialect, true, &mut fields)
662                .expect("splits")
663                .expect("a whole record");
664            rows.push(fields.clone());
665            at = next;
666        }
667        rows
668    }
669
670    fn comma() -> Dialect {
671        Dialect { delimiter: b',', quote: Some(b'"'), escape: Some(b'"'), header: true }
672    }
673
674    #[test]
675    fn a_line_of_fields_is_the_fields_of_that_line() {
676        assert_eq!(split(b"a,b,c\n1,2,3\n", comma()), [["a", "b", "c"], ["1", "2", "3"]]);
677    }
678
679    #[test]
680    fn the_last_line_does_not_need_a_newline_on_it() {
681        assert_eq!(split(b"a,b\n1,2", comma()), [["a", "b"], ["1", "2"]]);
682    }
683
684    #[test]
685    fn all_three_line_endings_end_a_line() {
686        assert_eq!(split(b"a\r\nb\rc\n", comma()), [["a"], ["b"], ["c"]]);
687    }
688
689    #[test]
690    fn a_quoted_field_may_hold_the_delimiter_and_a_newline() {
691        assert_eq!(split(b"1,\"x,y\"\n", comma()), [["1", "x,y"]]);
692        assert_eq!(split(b"1,\"x\ny\"\n", comma()), [["1", "x\ny"]]);
693    }
694
695    #[test]
696    fn a_doubled_quote_inside_a_quoted_field_is_one_quote() {
697        assert_eq!(split(b"1,\"say \"\"hi\"\"\"\n", comma()), [["1", "say \"hi\""]]);
698    }
699
700    #[test]
701    fn an_empty_field_is_an_empty_string_here_and_becomes_a_null_above() {
702        assert_eq!(split(b"1,,3\n", comma()), [["1", "", "3"]]);
703        assert_eq!(split(b"1,\"\",3\n", comma()), [["1", "", "3"]]);
704    }
705
706    #[test]
707    fn a_trailing_delimiter_makes_a_last_empty_field() {
708        assert_eq!(split(b"1|x|\n", Dialect { delimiter: b'|', ..comma() }), [["1", "x", ""]]);
709    }
710
711    #[test]
712    fn a_quote_in_the_middle_of_a_bare_field_is_just_a_character() {
713        assert_eq!(split(b"1,he said \"hi\"\n", comma()), [["1", "he said \"hi\""]]);
714    }
715
716    #[test]
717    fn a_record_that_the_buffer_does_not_hold_all_of_is_not_a_record_yet() {
718        let mut fields = Vec::new();
719        assert_eq!(record(b"a,b", 0, comma(), false, &mut fields).unwrap(), None);
720        assert_eq!(record(b"a,\"b", 0, comma(), true, &mut fields).unwrap(), None);
721        assert_eq!(record(b"a,b\n", 0, comma(), false, &mut fields).unwrap(), Some(4));
722    }
723
724    #[test]
725    fn rubbish_after_a_closing_quote_is_an_error_rather_than_a_guess() {
726        let mut fields = Vec::new();
727        let error = record(b"\"x\"y,2\n", 0, comma(), true, &mut fields).unwrap_err();
728        assert!(error.message().contains("quoted value"), "{error}");
729    }
730
731    #[test]
732    fn utf8_survives_being_read_one_byte_at_a_time() {
733        assert_eq!(split("a,héllo\n".as_bytes(), comma()), [["a", "héllo"]]);
734    }
735}
736
737/// The two readers held to the same answers.
738///
739/// [`record`] is the reference, because it is the loop the sniffer runs and the one every rule in
740/// this file was first written down in. Everything [`records`] reports is compared with it: the
741/// fields of every record, where the reading stopped, and the error if there was one.
742#[cfg(test)]
743mod agree {
744    use super::*;
745
746    /// What a whole scan found: the records, where it stopped, and the error it stopped on.
747    type Outcome = (Vec<Vec<String>>, usize, Option<String>);
748
749    fn by_record(bytes: &[u8], from: usize, dialect: Dialect, eof: bool) -> Outcome {
750        let mut rows = Vec::new();
751        let mut fields = Vec::new();
752        let mut at = from;
753        loop {
754            match record(bytes, at, dialect, eof, &mut fields) {
755                Ok(Some(next)) => {
756                    rows.push(fields.clone());
757                    at = next;
758                }
759                Ok(None) => return (rows, at, None),
760                Err(error) => return (rows, at, Some(error.to_string())),
761            }
762        }
763    }
764
765    fn by_chunk(bytes: &[u8], from: usize, dialect: Dialect, eof: bool, limit: usize) -> Outcome {
766        let mut rows = Vec::new();
767        let mut at = from;
768        let mut out = Records::default();
769        loop {
770            out.clear();
771            let result = records(bytes, at, dialect, eof, limit, &mut out);
772            for row in 0..out.len() {
773                rows.push(
774                    out.fields(row)
775                        .iter()
776                        .map(|span| span.text(bytes, dialect).into_owned())
777                        .collect(),
778                );
779            }
780            match result {
781                Ok(next) => {
782                    at = next;
783                    if out.len() < limit {
784                        return (rows, at, None);
785                    }
786                }
787                Err(error) => {
788                    // The error leaves the records before it in `out`, so the position is the
789                    // start of the record it was found in, which is where `record` stopped too.
790                    let mut from = at;
791                    let mut fields = Vec::new();
792                    for _ in 0..out.len() {
793                        from = record(bytes, from, dialect, eof, &mut fields).unwrap().unwrap();
794                    }
795                    return (rows, from, Some(error.to_string()));
796                }
797            }
798        }
799    }
800
801    /// Both readers over `bytes`, whole and cut at every one of `cuts` the way a refill cuts it,
802    /// with a few chunk sizes.
803    fn check(bytes: &[u8], dialect: Dialect, cuts: &[usize]) {
804        for eof in [true, false] {
805            let expected = by_record(bytes, 0, dialect, eof);
806            for limit in [1, 2, 3, 7, 8192] {
807                assert_eq!(
808                    by_chunk(bytes, 0, dialect, eof, limit),
809                    expected,
810                    "{:?} under {dialect:?}, eof {eof}, {limit} at a time",
811                    String::from_utf8_lossy(bytes),
812                );
813            }
814        }
815        let whole = by_record(bytes, 0, dialect, true);
816        for &cut in cuts {
817            let cut = cut.min(bytes.len());
818            // The front of the file as a buffer that more bytes will follow, and then the rest of
819            // it from wherever that stopped, which is what a refill does.
820            let (mut rows, at, error) = by_chunk(&bytes[..cut], 0, dialect, false, 8192);
821            if error.is_some() {
822                assert_eq!(error, whole.2, "an error in the front is the error in the whole");
823                continue;
824            }
825            let (rest, end, error) = by_chunk(bytes, at, dialect, true, 8192);
826            rows.extend(rest);
827            assert_eq!(
828                (rows, end, error),
829                whole,
830                "{:?} under {dialect:?} cut at {cut}",
831                String::from_utf8_lossy(bytes),
832            );
833        }
834    }
835
836    fn dialects() -> [Dialect; 5] {
837        let comma =
838            Dialect { delimiter: b',', quote: Some(b'"'), escape: Some(b'"'), header: false };
839        [
840            comma,
841            Dialect { quote: None, escape: None, ..comma },
842            Dialect { delimiter: b'|', ..comma },
843            Dialect { delimiter: b'\t', quote: Some(b'\''), escape: Some(b'\''), ..comma },
844            Dialect { escape: Some(b'\\'), ..comma },
845        ]
846    }
847
848    #[test]
849    fn the_tricky_ones_split_the_same_both_ways() {
850        let long = "x".repeat(61);
851        let cases: Vec<Vec<u8>> = vec![
852            b"".to_vec(),
853            b"\n".to_vec(),
854            b"\n\n".to_vec(),
855            b"a".to_vec(),
856            b"a,b,c\n1,2,3\n".to_vec(),
857            b"a,b\n1,2".to_vec(),
858            b"a\r\nb\rc\n".to_vec(),
859            b"a\r".to_vec(),
860            b"a\r\r\n\n".to_vec(),
861            b"1,\"x,y\"\n2,\"x\ny\"\n".to_vec(),
862            b"1,\"say \"\"hi\"\"\"\n".to_vec(),
863            b"1,,3\n1,\"\",3\n".to_vec(),
864            b"1|x|\n1,x,\n".to_vec(),
865            b"1,he said \"hi\"\n2,x\n".to_vec(),
866            b"1,he said \"hi\n2,x\n".to_vec(),
867            b"\"x\"y,2\n".to_vec(),
868            b"\"x\"\"\n".to_vec(),
869            b"\"x\"\"".to_vec(),
870            b"a,\"b".to_vec(),
871            b"a,\"b\"".to_vec(),
872            b"\"".to_vec(),
873            b"\"\"\"\"\n".to_vec(),
874            b"\"a\"\"\",b\n".to_vec(),
875            b"\"a\\\"b\",c\n\"a\\\\\"\n".to_vec(),
876            b"'a,b'\t'c''d'\n".to_vec(),
877            b"x\xffy,\"\xfe\"\"\"\n".to_vec(),
878            "h\u{e9}llo,w\u{f6}rld\n".as_bytes().to_vec(),
879            format!("{long},\"a\nb\",c\n{long}\r\n\"{long}\"\"{long}\",d\n").into_bytes(),
880            format!("{long}ab\r\n{long}abc\r\n").into_bytes(),
881            format!("\"{long}\"\"\",\"\n\"\n{long},x\"\n").into_bytes(),
882        ];
883        for bytes in &cases {
884            let cuts: Vec<usize> = (0..=bytes.len()).collect();
885            for dialect in dialects() {
886                check(bytes, dialect, &cuts);
887            }
888        }
889    }
890
891    /// A small generator, since the crate has no dependencies to take one from.
892    struct Rng(u64);
893
894    impl Rng {
895        fn next(&mut self) -> u64 {
896            self.0 ^= self.0 << 13;
897            self.0 ^= self.0 >> 7;
898            self.0 ^= self.0 << 17;
899            self.0
900        }
901
902        fn below(&mut self, n: usize) -> usize {
903            (self.next() % n as u64) as usize
904        }
905    }
906
907    /// A file that is mostly a CSV file, with the kinds of damage real ones have.
908    fn generate(rng: &mut Rng) -> Vec<u8> {
909        const PIECES: [&[u8]; 20] = [
910            b"a",
911            b"1",
912            b"-2.5",
913            b"xyz",
914            b",",
915            b"|",
916            b"\t",
917            b"\"",
918            b"\"\"",
919            b"'",
920            b"\\",
921            b"\n",
922            b"\r",
923            b"\r\n",
924            b" ",
925            b"\xc3\xa9",
926            b"\xff",
927            b"2020-01-02",
928            b"",
929            b"0123456789abcdef",
930        ];
931        let mut out = Vec::new();
932        let rows = rng.below(40);
933        for _ in 0..rows {
934            let fields = 1 + rng.below(5);
935            for field in 0..fields {
936                if field > 0 {
937                    out.push(b",,,|\t"[rng.below(5)]);
938                }
939                let mut body = Vec::new();
940                for _ in 0..rng.below(6) {
941                    let piece = PIECES[rng.below(PIECES.len())];
942                    // Long runs of plain bytes now and then, so that fields straddle the blocks.
943                    if rng.below(10) == 0 {
944                        body.extend(std::iter::repeat_n(b'q', rng.below(90)));
945                    }
946                    body.extend_from_slice(piece);
947                }
948                match rng.below(4) {
949                    0 => {
950                        out.push(b'"');
951                        for &byte in &body {
952                            if byte == b'"' {
953                                out.push(b'"');
954                            }
955                            out.push(byte);
956                        }
957                        out.push(b'"');
958                    }
959                    1 => {
960                        out.push(b'"');
961                        out.extend_from_slice(&body);
962                        out.push(b'"');
963                    }
964                    _ => out.extend_from_slice(&body),
965                }
966            }
967            out.extend_from_slice([&b"\n"[..], b"\r\n", b"\r"][rng.below(3)]);
968        }
969        if rng.below(3) == 0 {
970            out.truncate(out.len().saturating_sub(1 + rng.below(3)));
971        }
972        out
973    }
974
975    #[test]
976    fn thousands_of_generated_files_split_the_same_both_ways() {
977        let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
978        for _ in 0..3000 {
979            let bytes = generate(&mut rng);
980            let cuts: Vec<usize> = (0..4).map(|_| rng.below(bytes.len() + 1)).collect();
981            for dialect in dialects() {
982                check(&bytes, dialect, &cuts);
983            }
984        }
985    }
986}