Skip to main content

nsv/
lib.rs

1//! NSV (Newline-Separated Values) format implementation for Rust
2//!
3//! Fast implementation using memchr, with optional parallel parsing via rayon.
4//! See https://nsv-format.org for the specification.
5//!
6//! ## Parallel Parsing Strategy
7//!
8//! For files larger than 64KB, we use a chunked parallel approach:
9//! 1. Pick N evenly-spaced byte positions (one per CPU core)
10//! 2. For each, scan forward to the nearest `\n\n` row boundary — O(avg_row_len)
11//! 3. Each worker independently parses its chunk (boundary scan + cell split + unescape)
12//!
13//! This works because literal `0x0A` bytes in NSV are always structural (never escaped),
14//! so row alignment recovery from any byte position is a trivial forward scan.
15//! The sequential phase is O(N), not O(input_len) — all real work is parallel.
16//!
17//! For smaller files, we use a sequential fast path to avoid thread overhead.
18
19pub mod util;
20
21use memchr::memmem;
22#[cfg(feature = "parallel")]
23use rayon::prelude::*;
24
25use std::borrow::Cow;
26use std::io::{self, Read, Write};
27
28pub const VERSION: &str = env!("CARGO_PKG_VERSION");
29
30/// Threshold for using parallel parsing (64KB)
31const PARALLEL_THRESHOLD: usize = 64 * 1024;
32
33/// Decode an NSV string into a seqseq.
34pub fn decode(s: &str) -> Vec<Vec<String>> {
35    decode_bytes(s.as_bytes())
36        .into_iter()
37        .map(|row| {
38            row.into_iter()
39                .map(|cell| {
40                    // SAFETY: input was &str (valid UTF-8). NSV splitting and unescaping
41                    // only operate on ASCII bytes (0x0A, 0x5C, 0x6E), which cannot split
42                    // a multi-byte UTF-8 sequence. Each resulting cell is therefore valid UTF-8.
43                    unsafe { String::from_utf8_unchecked(cell.into_owned()) }
44                })
45                .collect()
46        })
47        .collect()
48}
49
50/// Decode raw bytes into a seqseq of byte slices.
51/// No encoding assumption — works with any ASCII-compatible encoding.
52///
53/// Cells are returned as `Cow<[u8]>` — borrowed when no unescaping was needed
54/// (zero-copy), owned when the cell contained escape sequences.
55pub fn decode_bytes<'a>(input: &'a [u8]) -> Vec<Vec<Cow<'a, [u8]>>> {
56    if input.is_empty() {
57        return Vec::new();
58    }
59
60    #[cfg(feature = "parallel")]
61    if input.len() >= PARALLEL_THRESHOLD {
62        return decode_bytes_parallel(input);
63    }
64
65    decode_bytes_sequential(input)
66}
67
68/// Sequential implementation for small inputs (byte-level).
69fn decode_bytes_sequential<'a>(input: &'a [u8]) -> Vec<Vec<Cow<'a, [u8]>>> {
70    let mut data = Vec::new();
71    let mut row: Vec<Cow<'a, [u8]>> = Vec::new();
72    let mut start = 0;
73
74    for (pos, &b) in input.iter().enumerate() {
75        if b == b'\n' {
76            if pos > start {
77                row.push(unescape_bytes(&input[start..pos]));
78            } else {
79                data.push(row);
80                row = Vec::new();
81            }
82            start = pos + 1;
83        }
84    }
85
86    if start < input.len() {
87        row.push(unescape_bytes(&input[start..]));
88    }
89
90    if !row.is_empty() {
91        data.push(row);
92    }
93
94    data
95}
96
97/// Chunked parallel implementation for large inputs (byte-level).
98///
99/// Splits the input into N equal-sized byte chunks (one per core), aligns each
100/// split point to the nearest `\n\n` row boundary, and parses each chunk
101/// independently. The sequential phase is O(N), not O(input_len).
102#[cfg(feature = "parallel")]
103fn decode_bytes_parallel<'a>(input: &'a [u8]) -> Vec<Vec<Cow<'a, [u8]>>> {
104    let num_threads = rayon::current_num_threads();
105    let chunk_size = input.len() / num_threads;
106
107    if chunk_size == 0 {
108        return decode_bytes_sequential(input);
109    }
110
111    // Find N-1 split points at \n\n boundaries near evenly-spaced positions.
112    // Cost: O(N * avg_row_len) — negligible compared to input size.
113    let finder = memmem::Finder::new(b"\n\n");
114    let mut splits = Vec::with_capacity(num_threads + 1);
115    splits.push(0usize);
116
117    for i in 1..num_threads {
118        let nominal = i * chunk_size;
119        if let Some(offset) = finder.find(&input[nominal..]) {
120            let split = nominal + offset + 2; // byte after \n\n
121            if split < input.len() {
122                splits.push(split);
123            }
124        }
125    }
126    splits.push(input.len());
127    splits.dedup();
128
129    if splits.len() <= 2 {
130        return decode_bytes_sequential(input);
131    }
132
133    // Parse each chunk in parallel. Each chunk starts at a row boundary
134    // (or byte 0), so the sequential parser produces correct results per chunk.
135    let chunks: Vec<&[u8]> = splits.windows(2).map(|w| &input[w[0]..w[1]]).collect();
136
137    let chunk_results: Vec<Vec<Vec<Cow<'a, [u8]>>>> = chunks
138        .par_iter()
139        .map(|chunk| decode_bytes_sequential(chunk))
140        .collect();
141
142    let total_rows: usize = chunk_results.iter().map(|r| r.len()).sum();
143    let mut result = Vec::with_capacity(total_rows);
144    for chunk_rows in chunk_results {
145        result.extend(chunk_rows);
146    }
147    result
148}
149
150/// Unescape a single NSV cell.
151///
152/// Returns `Cow::Borrowed` when no unescaping is needed.
153pub fn unescape(s: &str) -> Cow<'_, str> {
154    match unescape_bytes(s.as_bytes()) {
155        // SAFETY: input was &str (valid UTF-8). Borrowed means no transformation,
156        // so the result is the same valid UTF-8 slice.
157        Cow::Borrowed(b) => Cow::Borrowed(unsafe { std::str::from_utf8_unchecked(b) }),
158        // SAFETY: unescape only removes/replaces ASCII bytes — preserves UTF-8 validity.
159        Cow::Owned(v) => Cow::Owned(unsafe { String::from_utf8_unchecked(v) }),
160    }
161}
162
163/// Unescape a single raw cell (byte-level).
164///
165/// Interprets `\` as the empty cell token (returns empty vec).
166/// `\\` → `\`, `\n` → LF. Unrecognized sequences pass through.
167/// Dangling backslash at end is stripped.
168///
169/// Returns `Cow::Borrowed` when no unescaping is needed (no backslash present).
170pub fn unescape_bytes(s: &[u8]) -> Cow<'_, [u8]> {
171    if s == b"\\" {
172        return Cow::Owned(Vec::new());
173    }
174
175    if !s.contains(&b'\\') {
176        return Cow::Borrowed(s);
177    }
178
179    let mut out = Vec::with_capacity(s.len());
180    let mut escaped = false;
181
182    for &b in s {
183        if escaped {
184            match b {
185                b'n' => out.push(b'\n'),
186                b'\\' => out.push(b'\\'),
187                _ => {
188                    out.push(b'\\');
189                    out.push(b);
190                }
191            }
192            escaped = false;
193        } else if b == b'\\' {
194            escaped = true;
195        } else {
196            out.push(b);
197        }
198    }
199
200    Cow::Owned(out)
201}
202
203/// Escape a single NSV cell.
204///
205/// Returns `Cow::Borrowed` when no escaping is needed.
206pub fn escape(s: &str) -> Cow<'_, str> {
207    match escape_bytes(s.as_bytes()) {
208        // SAFETY: input was &str (valid UTF-8). Borrowed means no transformation,
209        // so the result is the same valid UTF-8 slice.
210        Cow::Borrowed(b) => Cow::Borrowed(unsafe { std::str::from_utf8_unchecked(b) }),
211        // SAFETY: escape only inserts ASCII bytes (\, n) — preserves UTF-8 validity.
212        Cow::Owned(v) => Cow::Owned(unsafe { String::from_utf8_unchecked(v) }),
213    }
214}
215
216/// Escape a single raw cell (byte-level).
217///
218/// Empty input → `\` (empty cell token).
219/// `\` → `\\`, LF → `\n`.
220///
221/// Returns `Cow::Borrowed` when no escaping is needed (non-empty, no `\` or LF).
222pub fn escape_bytes(s: &[u8]) -> Cow<'_, [u8]> {
223    if s.is_empty() {
224        return Cow::Owned(b"\\".to_vec());
225    }
226
227    if s.contains(&b'\n') || s.contains(&b'\\') {
228        let mut out = Vec::with_capacity(s.len() + s.len() / 4);
229        for &b in s {
230            match b {
231                b'\\' => {
232                    out.push(b'\\');
233                    out.push(b'\\');
234                }
235                b'\n' => {
236                    out.push(b'\\');
237                    out.push(b'n');
238                }
239                _ => out.push(b),
240            }
241        }
242        Cow::Owned(out)
243    } else {
244        Cow::Borrowed(s)
245    }
246}
247
248// ── Projected (column-selective) parsing ─────────────────────────────
249//
250// Single-pass scan that tracks the column index, skips non-projected
251// columns entirely (no allocation, no unescape), and directly produces
252// the final `Vec<Vec<Vec<u8>>>`.
253
254/// Build a column-map: `col_map[original_col] = projected_index`.
255/// Entries for non-projected columns are `usize::MAX`.
256fn build_col_map(columns: &[usize]) -> (Vec<usize>, usize) {
257    let max_col = columns.iter().copied().max().unwrap_or(0);
258    let mut col_map = vec![usize::MAX; max_col + 1];
259    for (proj_idx, &orig_col) in columns.iter().enumerate() {
260        col_map[orig_col] = proj_idx;
261    }
262    (col_map, max_col)
263}
264
265/// Decode only the specified columns from raw bytes.
266///
267/// Single-pass: scans for cell/row boundaries and directly unescapes
268/// only the cells in projected columns.  No intermediate structural index.
269/// Each inner vec has exactly `columns.len()` entries (same order as `columns`).
270///
271/// Cells are returned as `Cow<[u8]>` — borrowed when no unescaping was needed.
272pub fn decode_bytes_projected<'a>(input: &'a [u8], columns: &[usize]) -> Vec<Vec<Cow<'a, [u8]>>> {
273    if input.is_empty() || columns.is_empty() {
274        return Vec::new();
275    }
276
277    #[cfg(feature = "parallel")]
278    if input.len() >= PARALLEL_THRESHOLD {
279        return decode_projected_parallel(input, columns);
280    }
281
282    decode_projected_sequential(input, columns)
283}
284
285/// Sequential single-pass projected decode.
286fn decode_projected_sequential<'a>(input: &'a [u8], columns: &[usize]) -> Vec<Vec<Cow<'a, [u8]>>> {
287    let (col_map, max_col) = build_col_map(columns);
288    let stride = columns.len();
289    let mut data: Vec<Vec<Cow<'a, [u8]>>> = Vec::new();
290    let mut row: Vec<Cow<'a, [u8]>> = vec![Cow::Borrowed(b""); stride];
291    let mut col_idx: usize = 0;
292    let mut start = 0;
293    let mut row_has_cells = false;
294
295    for (pos, &b) in input.iter().enumerate() {
296        if b == b'\n' {
297            if pos > start {
298                if col_idx <= max_col {
299                    if let Some(&proj_idx) = col_map.get(col_idx) {
300                        if proj_idx != usize::MAX {
301                            row[proj_idx] = unescape_bytes(&input[start..pos]);
302                        }
303                    }
304                }
305                col_idx += 1;
306                row_has_cells = true;
307            } else {
308                if row_has_cells || !data.is_empty() || col_idx == 0 {
309                    data.push(row);
310                    row = vec![Cow::Borrowed(b""); stride];
311                }
312                col_idx = 0;
313                row_has_cells = false;
314            }
315            start = pos + 1;
316        }
317    }
318
319    if start < input.len() {
320        if col_idx <= max_col {
321            if let Some(&proj_idx) = col_map.get(col_idx) {
322                if proj_idx != usize::MAX {
323                    row[proj_idx] = unescape_bytes(&input[start..]);
324                }
325            }
326        }
327        row_has_cells = true;
328    }
329
330    if row_has_cells {
331        data.push(row);
332    }
333
334    data
335}
336
337/// Parallel single-pass projected decode.
338#[cfg(feature = "parallel")]
339fn decode_projected_parallel<'a>(input: &'a [u8], columns: &[usize]) -> Vec<Vec<Cow<'a, [u8]>>> {
340    let num_threads = rayon::current_num_threads();
341    let chunk_size = input.len() / num_threads;
342
343    if chunk_size == 0 {
344        return decode_projected_sequential(input, columns);
345    }
346
347    let finder = memmem::Finder::new(b"\n\n");
348    let mut splits = Vec::with_capacity(num_threads + 1);
349    splits.push(0usize);
350
351    for i in 1..num_threads {
352        let nominal = i * chunk_size;
353        if let Some(offset) = finder.find(&input[nominal..]) {
354            let split = nominal + offset + 2;
355            if split < input.len() {
356                splits.push(split);
357            }
358        }
359    }
360    splits.push(input.len());
361    splits.dedup();
362
363    if splits.len() <= 2 {
364        return decode_projected_sequential(input, columns);
365    }
366
367    let chunks: Vec<&[u8]> = splits.windows(2).map(|w| &input[w[0]..w[1]]).collect();
368
369    let chunk_results: Vec<Vec<Vec<Cow<'a, [u8]>>>> = chunks
370        .par_iter()
371        .map(|chunk| decode_projected_sequential(chunk, columns))
372        .collect();
373
374    let total_rows: usize = chunk_results.iter().map(|r| r.len()).sum();
375    let mut result = Vec::with_capacity(total_rows);
376    for chunk_rows in chunk_results {
377        result.extend(chunk_rows);
378    }
379    result
380}
381
382/// Encode a seqseq into an NSV string.
383pub fn encode(data: &[Vec<String>]) -> String {
384    let mut result = Vec::new();
385
386    for row in data {
387        for cell in row {
388            result.extend_from_slice(&escape_bytes(cell.as_bytes()));
389            result.push(b'\n');
390        }
391        result.push(b'\n');
392    }
393
394    // Safety: encoding only inserts ASCII bytes (\, n, LF) — preserves UTF-8.
395    String::from_utf8(result).unwrap()
396}
397
398/// Encode a seqseq of byte vectors into raw NSV bytes.
399pub fn encode_bytes(data: &[Vec<Vec<u8>>]) -> Vec<u8> {
400    let mut result = Vec::new();
401
402    for row in data {
403        for cell in row {
404            result.extend_from_slice(&escape_bytes(cell));
405            result.push(b'\n');
406        }
407        result.push(b'\n');
408    }
409
410    result
411}
412
413/// A single warning found during validation.
414#[derive(Debug, Clone, PartialEq, Eq)]
415pub struct Warning {
416    pub kind: WarningKind,
417    pub pos: usize,
418    pub line: usize,
419    pub col: usize,
420}
421
422#[derive(Debug, Clone, PartialEq, Eq)]
423pub enum WarningKind {
424    /// `\` followed by a byte other than `n` or `\`
425    UnknownEscape(u8),
426    /// `\` immediately before LF or at EOF
427    DanglingBackslash,
428    /// Non-empty input not ending with LF
429    NoTerminalLf,
430}
431
432/// Report edge cases in raw NSV input without altering parsing behavior.
433///
434/// Warns on unknown escape sequences, dangling backslashes, and missing terminal LF.
435/// Positions are byte offsets; line and col are 1-indexed.
436pub fn check(input: &[u8]) -> Vec<Warning> {
437    if input.is_empty() {
438        return Vec::new();
439    }
440
441    let mut warnings = Vec::new();
442    let len = input.len();
443    let mut line: usize = 1;
444    let mut line_start: usize = 0;
445    let mut escaped = false;
446
447    for (i, &b) in input.iter().enumerate() {
448        if escaped {
449            match b {
450                b'n' | b'\\' => {}
451                b'\n' if i - 1 != line_start => warnings.push(Warning {
452                    kind: WarningKind::DanglingBackslash,
453                    pos: i - 1,
454                    line,
455                    col: i - line_start,
456                }),
457                b'\n' => {}
458                _ => warnings.push(Warning {
459                    kind: WarningKind::UnknownEscape(b),
460                    pos: i - 1,
461                    line,
462                    col: i - line_start,
463                }),
464            }
465            escaped = false;
466        } else if b == b'\\' {
467            escaped = true;
468        }
469        if b == b'\n' {
470            line += 1;
471            line_start = i + 1;
472        }
473    }
474
475    if escaped {
476        warnings.push(Warning {
477            kind: WarningKind::DanglingBackslash,
478            pos: len - 1,
479            line,
480            col: len - line_start,
481        });
482    }
483
484    if line_start != len {
485        warnings.push(Warning {
486            kind: WarningKind::NoTerminalLf,
487            pos: len,
488            line,
489            col: len - line_start + 1,
490        });
491    }
492
493    warnings
494}
495
496// ── Streaming ────────────────────────────────────────────────────────
497
498/// Streaming NSV reader. Yields one complete row of byte vectors at a time.
499///
500/// On EOF, returns `Ok(None)` without discarding buffered state — calling
501/// `next_row()` again after more data arrives resumes where it left off.
502pub struct Reader<R> {
503    inner: io::BufReader<R>,
504    line_buf: Vec<u8>,
505    row: Vec<Vec<u8>>,
506}
507
508impl<R: io::Read> Reader<R> {
509    pub fn new(reader: R) -> Self {
510        Self::from_buf_reader(io::BufReader::new(reader))
511    }
512
513    pub fn from_buf_reader(reader: io::BufReader<R>) -> Self {
514        Reader { inner: reader, line_buf: Vec::new(), row: Vec::new() }
515    }
516
517    pub fn next_row(&mut self) -> io::Result<Option<Vec<Vec<u8>>>> {
518        let mut byte = [0u8; 1];
519        loop {
520            match self.inner.read(&mut byte) {
521                Ok(0) => return Ok(None),
522                Err(e) => return Err(e),
523                Ok(_) if byte[0] != b'\n' => self.line_buf.push(byte[0]),
524                Ok(_) if self.line_buf.is_empty() => return Ok(Some(std::mem::take(&mut self.row))),
525                Ok(_) => {
526                    self.row.push(unescape_bytes(&self.line_buf).into_owned());
527                    self.line_buf.clear();
528                }
529            }
530        }
531    }
532
533    /// Completed cells of the row currently being assembled.
534    pub fn partial_row(&self) -> &[Vec<u8>] {
535        &self.row
536    }
537
538    /// Bytes accumulated for the cell currently being read (not yet unescaped).
539    pub fn partial_cell(&self) -> &[u8] {
540        &self.line_buf
541    }
542
543    /// Recover the inner `BufReader`.
544    pub fn into_inner(self) -> io::BufReader<R> {
545        self.inner
546    }
547}
548
549impl<R: io::Read> Iterator for Reader<R> {
550    type Item = io::Result<Vec<Vec<u8>>>;
551    fn next(&mut self) -> Option<Self::Item> {
552        self.next_row().transpose()
553    }
554}
555
556/// Streaming NSV writer. Wraps any `W: Write` and writes one row at a time.
557///
558/// No internal buffering — wrap the inner writer in `BufWriter` if needed.
559pub struct Writer<W> {
560    inner: W,
561}
562
563impl<W: Write> Writer<W> {
564    pub fn new(writer: W) -> Self {
565        Writer { inner: writer }
566    }
567
568    /// Write a single complete row. Each cell is escaped and `\n`-terminated;
569    /// an extra `\n` terminates the row.
570    ///
571    /// Accepts any cell type that implements `AsRef<[u8]>`: `&[u8]`, `Vec<u8>`,
572    /// `&str`, `String`, etc.
573    pub fn write_row<C: AsRef<[u8]>>(&mut self, row: &[C]) -> io::Result<()> {
574        for cell in row {
575            self.inner.write_all(&escape_bytes(cell.as_ref()))?;
576            self.inner.write_all(b"\n")?;
577        }
578        self.inner.write_all(b"\n")
579    }
580
581    /// Recover the inner writer.
582    pub fn into_inner(self) -> W {
583        self.inner
584    }
585}
586
587#[cfg(test)]
588mod tests {
589    use super::*;
590
591    /// Convert Cow cells to owned for comparison with pre-built Vec<Vec<Vec<u8>>> data.
592    fn owned(rows: Vec<Vec<Cow<[u8]>>>) -> Vec<Vec<Vec<u8>>> {
593        rows.into_iter()
594            .map(|row| row.into_iter().map(|c| c.into_owned()).collect())
595            .collect()
596    }
597
598    #[test]
599    fn test_simple_table() {
600        let nsv = "col1\ncol2\n\na\nb\n\nc\nd\n";
601        let result = decode(nsv);
602        assert_eq!(
603            result,
604            vec![
605                vec!["col1".to_string(), "col2".to_string()],
606                vec!["a".to_string(), "b".to_string()],
607                vec!["c".to_string(), "d".to_string()],
608            ]
609        );
610    }
611
612    #[test]
613    fn test_empty_fields() {
614        let nsv = "a\n\\\nb\n\n\\\nc\n\\\n";
615        let result = decode(nsv);
616        assert_eq!(
617            result,
618            vec![
619                vec!["a".to_string(), "".to_string(), "b".to_string()],
620                vec!["".to_string(), "c".to_string(), "".to_string()],
621            ]
622        );
623    }
624
625    #[test]
626    fn test_escape_sequences() {
627        let nsv = "Line 1\\nLine 2\nBackslash: \\\\\nNot a newline: \\\\n\n";
628        let result = decode(nsv);
629        assert_eq!(
630            result,
631            vec![vec![
632                "Line 1\nLine 2".to_string(),
633                "Backslash: \\".to_string(),
634                "Not a newline: \\n".to_string()
635            ],]
636        );
637    }
638
639    #[test]
640    fn test_empty_rows() {
641        let nsv = "first\n\n\n\nsecond\n";
642        let result = decode(nsv);
643        assert_eq!(
644            result,
645            vec![
646                vec!["first".to_string()],
647                vec![],
648                vec![],
649                vec!["second".to_string()],
650            ]
651        );
652    }
653
654    #[test]
655    fn test_multiple_empty_rows() {
656        let nsv = "a\n\n\n\n\nb\n";
657        let result = decode(nsv);
658        assert_eq!(
659            result,
660            vec![
661                vec!["a".to_string()],
662                vec![],
663                vec![],
664                vec![],
665                vec!["b".to_string()],
666            ]
667        );
668    }
669
670    #[test]
671    fn test_roundtrip() {
672        let original = vec![
673            vec!["col1".to_string(), "col2".to_string()],
674            vec!["a".to_string(), "b".to_string()],
675            vec!["".to_string(), "value\\with\\backslash".to_string()],
676            vec!["multi\nline".to_string(), "normal".to_string()],
677        ];
678
679        let encoded = encode(&original);
680        let decoded = decode(&encoded);
681        assert_eq!(original, decoded);
682    }
683
684    #[test]
685    fn test_unrecognized_escape() {
686        let nsv = "\\x41\\t\\r\n";
687        let result = decode(nsv);
688        assert_eq!(result, vec![vec!["\\x41\\t\\r".to_string()],]);
689    }
690
691    #[test]
692    fn test_dangling_backslash() {
693        let nsv = "text\\\n";
694        let result = decode(nsv);
695        assert_eq!(result, vec![vec!["text".to_string()],]);
696    }
697
698    #[test]
699    fn test_empty_input() {
700        let result = decode("");
701        assert_eq!(result, Vec::<Vec<String>>::new());
702    }
703
704    #[test]
705    fn test_no_trailing_newline() {
706        let nsv = "a\nb";
707        let result = decode(nsv);
708        assert_eq!(result, vec![vec!["a".to_string(), "b".to_string()],]);
709    }
710
711    #[test]
712    fn test_only_empty_rows() {
713        let nsv = "\n\n\n\n";
714        let result = decode(nsv);
715        assert_eq!(
716            result,
717            vec![
718                Vec::<String>::new(),
719                Vec::<String>::new(),
720                Vec::<String>::new(),
721                Vec::<String>::new(),
722            ]
723        );
724    }
725
726    #[test]
727    fn test_starts_with_empty_row() {
728        let nsv = "\n\nfirst\n";
729        let result = decode(nsv);
730        assert_eq!(
731            result,
732            vec![
733                Vec::<String>::new(),
734                Vec::<String>::new(),
735                vec!["first".to_string()],
736            ]
737        );
738    }
739
740    #[test]
741    fn test_large_file() {
742        // Generate ~10MB of data to verify parallel path is exercised
743        // (needs to exceed PARALLEL_THRESHOLD of 64KB)
744        let large_data: Vec<Vec<String>> = (0..100_000)
745            .map(|i| vec![format!("row{}", i), format!("data{}", i)])
746            .collect();
747
748        let encoded = encode(&large_data);
749
750        // Verify it's large enough to trigger parallel parsing
751        assert!(encoded.len() > PARALLEL_THRESHOLD);
752
753        let decoded = decode(&encoded);
754        assert_eq!(large_data, decoded);
755    }
756
757    #[test]
758    fn test_parallel_with_empty_rows() {
759        // Test parallel path with empty rows mixed in
760        let mut data = Vec::new();
761
762        // Create enough data to exceed 64KB threshold
763        for i in 0..10_000 {
764            data.push(vec![format!("value{}", i)]);
765
766            // Add empty row every 100 rows
767            if i % 100 == 0 {
768                data.push(vec![]);
769            }
770        }
771
772        let encoded = encode(&data);
773        assert!(encoded.len() > PARALLEL_THRESHOLD);
774
775        let decoded = decode(&encoded);
776        assert_eq!(data, decoded);
777    }
778
779    #[test]
780    fn test_parallel_with_escape_sequences() {
781        // Test parallel path with cells containing escape sequences
782        let mut data = Vec::new();
783
784        for i in 0..10_000 {
785            data.push(vec![
786                format!("Line 1\nLine 2 {}", i),
787                format!("Backslash: \\ {}", i),
788                "".to_string(),
789            ]);
790        }
791
792        let encoded = encode(&data);
793        assert!(encoded.len() > PARALLEL_THRESHOLD);
794
795        let decoded = decode(&encoded);
796        assert_eq!(data, decoded);
797    }
798
799    // ── Byte-level tests ──
800
801    #[test]
802    fn test_bytes_roundtrip() {
803        let original: Vec<Vec<Vec<u8>>> = vec![
804            vec![b"col1".to_vec(), b"col2".to_vec()],
805            vec![b"a".to_vec(), b"b".to_vec()],
806            vec![b"".to_vec(), b"value\\with\\backslash".as_slice().to_vec()],
807            vec![b"multi\nline".to_vec(), b"normal".to_vec()],
808        ];
809
810        let encoded = encode_bytes(&original);
811        let decoded = owned(decode_bytes(&encoded));
812        assert_eq!(original, decoded);
813    }
814
815    #[test]
816    fn test_bytes_non_utf8() {
817        // Latin-1 bytes with values > 0x7F — not valid UTF-8
818        let cell1: Vec<u8> = vec![0xC0, 0xE9, 0xF1]; // àéñ in Latin-1
819        let cell2: Vec<u8> = vec![0xFF, 0xFE, 0x80]; // arbitrary high bytes
820        let original: Vec<Vec<Vec<u8>>> = vec![vec![cell1.clone(), cell2.clone()]];
821
822        let encoded = encode_bytes(&original);
823        let decoded = owned(decode_bytes(&encoded));
824        assert_eq!(original, decoded);
825    }
826
827    #[test]
828    fn test_bytes_empty_cells_and_rows() {
829        let original: Vec<Vec<Vec<u8>>> = vec![
830            vec![b"a".to_vec(), b"".to_vec(), b"b".to_vec()],
831            vec![],
832            vec![b"".to_vec()],
833        ];
834
835        let encoded = encode_bytes(&original);
836        let decoded = owned(decode_bytes(&encoded));
837        assert_eq!(original, decoded);
838    }
839
840    #[test]
841    fn test_bytes_with_special_ascii_bytes() {
842        // Verify that only 0x0A (LF), 0x5C (\), 0x6E (n) are structurally significant
843        let cell: Vec<u8> = (0u8..=255)
844            .filter(|&b| b != b'\n' && b != b'\\')
845            .collect();
846        let original = vec![vec![cell.clone()]];
847
848        let encoded = encode_bytes(&original);
849        let decoded = owned(decode_bytes(&encoded));
850        assert_eq!(original, decoded);
851    }
852
853    #[test]
854    fn test_decode_bytes_matches_decode_str() {
855        // Confirm decode(s) and decode_bytes(s.as_bytes()) produce equivalent structures
856        let inputs = vec![
857            "col1\ncol2\n\na\nb\n\nc\nd\n",
858            "a\n\\\nb\n\n\\\nc\n\\\n",
859            "Line 1\\nLine 2\nBackslash: \\\\\n",
860            "first\n\n\n\nsecond\n",
861            "",
862            "\n\n\n\n",
863        ];
864
865        for input in inputs {
866            let str_result = decode(input);
867            let byte_result = decode_bytes(input.as_bytes());
868
869            // Convert byte result to strings for comparison
870            let byte_as_str: Vec<Vec<String>> = byte_result
871                .into_iter()
872                .map(|row| {
873                    row.into_iter()
874                        .map(|cell| String::from_utf8(cell.into_owned()).unwrap())
875                        .collect()
876                })
877                .collect();
878
879            assert_eq!(str_result, byte_as_str, "mismatch for input: {:?}", input);
880        }
881    }
882
883    #[test]
884    fn test_bytes_large_parallel() {
885        // Generate enough data to trigger parallel path
886        let large_data: Vec<Vec<Vec<u8>>> = (0..100_000)
887            .map(|i| {
888                vec![
889                    format!("row{}", i).into_bytes(),
890                    format!("data{}", i).into_bytes(),
891                ]
892            })
893            .collect();
894
895        let encoded = encode_bytes(&large_data);
896        assert!(encoded.len() > PARALLEL_THRESHOLD);
897
898        let decoded = owned(decode_bytes(&encoded));
899        assert_eq!(large_data, decoded);
900    }
901
902    // ── check() tests ──
903
904    #[test]
905    fn test_check_empty_input() {
906        assert_eq!(check(b""), vec![]);
907    }
908
909    #[test]
910    fn test_check_just_lf() {
911        assert_eq!(check(b"\n"), vec![]);
912    }
913
914    #[test]
915    fn test_check_no_issues() {
916        assert_eq!(check(b"col1\ncol2\n\na\nb\n\n"), vec![]);
917        assert_eq!(check(b"hello\\\\world\n\\n\n\n"), vec![]);
918    }
919
920    #[test]
921    fn test_check_single_unknown_escape() {
922        let warnings = check(b"hello\\tworld\n");
923        assert_eq!(
924            warnings,
925            vec![Warning {
926                kind: WarningKind::UnknownEscape(b't'),
927                pos: 5,
928                line: 1,
929                col: 6,
930            }]
931        );
932    }
933
934    #[test]
935    fn test_check_multiple_unknown_escapes_different_lines() {
936        let warnings = check(b"\\thello\n\\rworld\n\n");
937        assert_eq!(
938            warnings,
939            vec![
940                Warning {
941                    kind: WarningKind::UnknownEscape(b't'),
942                    pos: 0,
943                    line: 1,
944                    col: 1,
945                },
946                Warning {
947                    kind: WarningKind::UnknownEscape(b'r'),
948                    pos: 8,
949                    line: 2,
950                    col: 1,
951                },
952            ]
953        );
954    }
955
956    #[test]
957    fn test_check_dangling_backslash_mid_file() {
958        let warnings = check(b"text\\\nmore\n\n");
959        assert_eq!(
960            warnings,
961            vec![Warning {
962                kind: WarningKind::DanglingBackslash,
963                pos: 4,
964                line: 1,
965                col: 5,
966            }]
967        );
968    }
969
970    #[test]
971    fn test_check_dangling_backslash_at_eof() {
972        let warnings = check(b"text\\");
973        assert_eq!(
974            warnings,
975            vec![
976                Warning {
977                    kind: WarningKind::DanglingBackslash,
978                    pos: 4,
979                    line: 1,
980                    col: 5,
981                },
982                Warning {
983                    kind: WarningKind::NoTerminalLf,
984                    pos: 5,
985                    line: 1,
986                    col: 6,
987                },
988            ]
989        );
990    }
991
992    #[test]
993    fn test_check_empty_cell_token_no_warning() {
994        assert_eq!(check(b"\\\n\n"), vec![]);
995    }
996
997    #[test]
998    fn test_check_empty_cell_token_unterminated_row() {
999        let warnings = check(b"\\\n");
1000        assert!(
1001            !warnings
1002                .iter()
1003                .any(|w| w.kind == WarningKind::DanglingBackslash),
1004            "empty-cell token must not warn as dangling backslash: {warnings:?}"
1005        );
1006    }
1007
1008    #[test]
1009    fn test_check_dangling_backslash_with_content_still_warns() {
1010        let warnings = check(b"text\\\n\n");
1011        assert_eq!(
1012            warnings,
1013            vec![Warning {
1014                kind: WarningKind::DanglingBackslash,
1015                pos: 4,
1016                line: 1,
1017                col: 5,
1018            }]
1019        );
1020    }
1021
1022    #[test]
1023    fn test_check_empty_cell_and_dangling_mixed() {
1024        let warnings = check(b"\\\nabc\\\n\n");
1025        assert_eq!(
1026            warnings,
1027            vec![Warning {
1028                kind: WarningKind::DanglingBackslash,
1029                pos: 5,
1030                line: 2,
1031                col: 4,
1032            }]
1033        );
1034    }
1035
1036    #[test]
1037    fn test_check_no_terminal_lf() {
1038        let warnings = check(b"hello");
1039        assert_eq!(
1040            warnings,
1041            vec![Warning {
1042                kind: WarningKind::NoTerminalLf,
1043                pos: 5,
1044                line: 1,
1045                col: 6,
1046            }]
1047        );
1048    }
1049
1050    #[test]
1051    fn test_check_combination() {
1052        // \(0) t(1) h(2) e(3) l(4) l(5) o(6) \(7) LF(8) w(9) o(10) r(11) l(12) d(13)
1053        let warnings = check(b"\\thello\\\nworld");
1054        assert_eq!(
1055            warnings,
1056            vec![
1057                Warning {
1058                    kind: WarningKind::UnknownEscape(b't'),
1059                    pos: 0,
1060                    line: 1,
1061                    col: 1,
1062                },
1063                Warning {
1064                    kind: WarningKind::DanglingBackslash,
1065                    pos: 7,
1066                    line: 1,
1067                    col: 8,
1068                },
1069                Warning {
1070                    kind: WarningKind::NoTerminalLf,
1071                    pos: 14,
1072                    line: 2,
1073                    col: 6,
1074                },
1075            ]
1076        );
1077    }
1078
1079    #[test]
1080    fn test_check_non_utf8() {
1081        // Non-UTF-8 bytes with a bad escape: 0xFF 0xFE \t 0x80 LF
1082        let input: &[u8] = &[0xFF, 0xFE, b'\\', b't', 0x80, b'\n'];
1083        let warnings = check(input);
1084        assert_eq!(
1085            warnings,
1086            vec![Warning {
1087                kind: WarningKind::UnknownEscape(b't'),
1088                pos: 2,
1089                line: 1,
1090                col: 3,
1091            }]
1092        );
1093    }
1094
1095    // ── Projected decode tests ──
1096
1097    #[test]
1098    fn test_project_subset() {
1099        let nsv = b"c0\nc1\nc2\nc3\n\na\nb\nc\nd\n\ne\nf\ng\nh\n\n";
1100        let projected = owned(decode_bytes_projected(nsv, &[0, 2]));
1101        assert_eq!(projected.len(), 3);
1102        assert_eq!(projected[0], vec![b"c0".to_vec(), b"c2".to_vec()]);
1103        assert_eq!(projected[1], vec![b"a".to_vec(), b"c".to_vec()]);
1104        assert_eq!(projected[2], vec![b"e".to_vec(), b"g".to_vec()]);
1105    }
1106
1107    #[test]
1108    fn test_project_single_column() {
1109        let nsv = b"name\nage\nsalary\n\nAlice\n30\n50000\n\nBob\n25\n75000\n\n";
1110        let projected = owned(decode_bytes_projected(nsv, &[1]));
1111        assert_eq!(projected.len(), 3);
1112        assert_eq!(projected[0], vec![b"age".to_vec()]);
1113        assert_eq!(projected[1], vec![b"30".to_vec()]);
1114        assert_eq!(projected[2], vec![b"25".to_vec()]);
1115    }
1116
1117    #[test]
1118    fn test_project_reorder() {
1119        let nsv = b"a\nb\nc\n\n1\n2\n3\n\n";
1120        let projected = owned(decode_bytes_projected(nsv, &[2, 0]));
1121        assert_eq!(projected[0], vec![b"c".to_vec(), b"a".to_vec()]);
1122        assert_eq!(projected[1], vec![b"3".to_vec(), b"1".to_vec()]);
1123    }
1124
1125    #[test]
1126    fn test_project_out_of_range() {
1127        let nsv = b"a\nb\n\n";
1128        let projected = owned(decode_bytes_projected(nsv, &[0, 5]));
1129        assert_eq!(projected[0], vec![b"a".to_vec(), b"".to_vec()]);
1130    }
1131
1132    #[test]
1133    fn test_projected_matches_full() {
1134        let nsv = b"c0\nc1\nc2\n\na\nb\nc\n\n";
1135        let full = owned(decode_bytes(nsv));
1136        let projected = owned(decode_bytes_projected(nsv, &[0, 1, 2]));
1137        assert_eq!(projected, full);
1138    }
1139
1140    #[test]
1141    fn test_project_with_escapes_parallel() {
1142        let mut data = Vec::new();
1143        for i in 0..10_000 {
1144            data.push(vec![
1145                format!("Line 1\nLine 2 {}", i),
1146                format!("Backslash: \\ {}", i),
1147                format!("plain{}", i),
1148            ]);
1149        }
1150        let encoded = encode(&data);
1151        let encoded_bytes = encoded.as_bytes();
1152        assert!(encoded_bytes.len() > PARALLEL_THRESHOLD);
1153
1154        let projected = decode_bytes_projected(encoded_bytes, &[2]);
1155        assert_eq!(projected.len(), data.len());
1156        for (ri, row) in data.iter().enumerate() {
1157            assert_eq!(
1158                String::from_utf8(projected[ri][0].to_vec()).unwrap(),
1159                row[2]
1160            );
1161        }
1162
1163        let full = owned(decode_bytes(encoded_bytes));
1164        let projected_all = owned(decode_bytes_projected(encoded_bytes, &[0, 1, 2]));
1165        assert_eq!(projected_all, full);
1166    }
1167
1168    // ── Streaming tests ──
1169
1170    use std::io::Cursor;
1171
1172    #[test]
1173    fn test_bytes_reader_matches_batch() {
1174        for input in [
1175            &b"a\nb\n\nc\nd\n\n"[..],
1176            b"a\n\\\nb\n\n\\\nc\n\\\n\n",          // empty cells
1177            b"Line 1\\nLine 2\n\\\\\n\\\\n\n\n",    // escapes
1178            b"first\n\n\n\nsecond\n\n",              // consecutive empty rows
1179            b"\\\n\\\n\\\n\n",                       // only empty cells
1180            b"\n\n\n\n",                             // only empty rows
1181            b"",
1182        ] {
1183            let streaming: Vec<_> = Reader::new(Cursor::new(input))
1184                .map(|r| r.unwrap())
1185                .collect();
1186            assert_eq!(streaming, owned(decode_bytes(input)), "input: {:?}", input);
1187        }
1188    }
1189
1190    #[test]
1191    fn test_bytes_reader_incomplete_row_not_emitted() {
1192        let mut r = Reader::new(Cursor::new(&b"a\nb\n\nc\nd"[..]));
1193        assert_eq!(r.next_row().unwrap(), Some(vec![b"a".to_vec(), b"b".to_vec()]));
1194        assert_eq!(r.next_row().unwrap(), None); // "c\nd" buffered, not emitted
1195    }
1196
1197    // ── Resumable ──
1198
1199    use std::cell::RefCell;
1200
1201    struct GrowableStream(RefCell<(Vec<u8>, usize)>);
1202    impl GrowableStream {
1203        fn new() -> Self { GrowableStream(RefCell::new((Vec::new(), 0))) }
1204        fn append(&self, b: &[u8]) { self.0.borrow_mut().0.extend_from_slice(b); }
1205    }
1206    impl io::Read for &GrowableStream {
1207        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1208            let mut s = self.0.borrow_mut();
1209            let n = buf.len().min(s.0.len() - s.1);
1210            buf[..n].copy_from_slice(&s.0[s.1..s.1 + n]);
1211            s.1 += n;
1212            Ok(n)
1213        }
1214    }
1215
1216    #[test]
1217    fn test_bytes_reader_resumable() {
1218        let s = GrowableStream::new();
1219        let mut r = Reader::new(&s);
1220
1221        // Partial row, then complete it
1222        s.append(b"a\nb\n\nc\n");
1223        assert_eq!(r.next_row().unwrap(), Some(vec![b"a".to_vec(), b"b".to_vec()]));
1224        assert_eq!(r.next_row().unwrap(), None);
1225        s.append(b"d\n\n");
1226        assert_eq!(r.next_row().unwrap(), Some(vec![b"c".to_vec(), b"d".to_vec()]));
1227
1228        // Mid-line split
1229        s.append(b"hel");
1230        assert_eq!(r.next_row().unwrap(), None);
1231        s.append(b"lo\n\n");
1232        assert_eq!(r.next_row().unwrap(), Some(vec![b"hello".to_vec()]));
1233    }
1234
1235    // ── Reader ──
1236
1237    #[test]
1238    fn test_bytes_reader() {
1239        for input in [
1240            &b"col1\ncol2\n\na\nb\n\nc\nd\n\n"[..],
1241            b"\n\n\n\n", b"", b"text\\\n\n",
1242        ] {
1243            let streaming: Vec<_> = Reader::new(Cursor::new(input))
1244                .map(|r| r.unwrap())
1245                .collect();
1246            assert_eq!(streaming, owned(decode_bytes(input)));
1247        }
1248        // Non-UTF-8 round-trip
1249        let orig = vec![vec![vec![0xC0, 0xE9], vec![0xFF, 0xFE]]];
1250        let enc = encode_bytes(&orig);
1251        let dec: Vec<_> = Reader::new(Cursor::new(&enc[..]))
1252            .map(|r| r.unwrap())
1253            .collect();
1254        assert_eq!(dec, orig);
1255    }
1256
1257    // ── Writer ──
1258
1259    #[test]
1260    fn test_writer() {
1261        let mut buf = Vec::new();
1262        let mut w = Writer::new(&mut buf);
1263        w.write_row(&["hello", "world"]).unwrap();
1264        assert_eq!(buf, b"hello\nworld\n\n");
1265
1266        buf.clear();
1267        Writer::new(&mut buf).write_row(&["line1\nline2", "back\\slash"]).unwrap();
1268        assert_eq!(buf, b"line1\\nline2\nback\\\\slash\n\n");
1269
1270        buf.clear();
1271        Writer::new(&mut buf).write_row(&["", "", ""]).unwrap();
1272        assert_eq!(buf, b"\\\n\\\n\\\n\n");
1273
1274        buf.clear();
1275        let empty: &[&str] = &[];
1276        Writer::new(&mut buf).write_row(empty).unwrap();
1277        assert_eq!(buf, b"\n");
1278    }
1279
1280    #[test]
1281    fn test_writer_matches_batch_encode() {
1282        let data = vec![
1283            vec!["a".to_string(), "b".to_string()],
1284            vec!["".to_string()],
1285            vec!["line\none".to_string(), "back\\slash".to_string()],
1286        ];
1287        let mut buf = Vec::new();
1288        {
1289            let mut w = Writer::new(&mut buf);
1290            for row in &data { w.write_row(row).unwrap(); }
1291        }
1292        assert_eq!(buf, encode(&data).as_bytes());
1293    }
1294
1295    // ── Round-trip ──
1296
1297    #[test]
1298    fn test_roundtrip_streaming() {
1299        let original: Vec<Vec<Vec<u8>>> = vec![
1300            vec![b"a".to_vec(), b"b".to_vec()],
1301            vec![b"".to_vec(), b"val\\ue".to_vec()],
1302            vec![b"multi\nline".to_vec(), b"normal".to_vec()],
1303            vec![],
1304        ];
1305        let mut buf = Vec::new();
1306        {
1307            let mut w = Writer::new(&mut buf);
1308            for row in &original {
1309                let refs: Vec<&[u8]> = row.iter().map(|c| c.as_slice()).collect();
1310                w.write_row(&refs).unwrap();
1311            }
1312        }
1313        let decoded: Vec<_> = Reader::new(Cursor::new(&buf[..]))
1314            .map(|r| r.unwrap())
1315            .collect();
1316        assert_eq!(decoded, original);
1317    }
1318}