Skip to main content

mmap_chunker_core/
scanner.rs

1/// Find chunk boundaries in `data` using the given delimiter.
2///
3/// Walks through `data` and creates sequential chunks that cover the entire
4/// input. Each chunk starts where the previous chunk ended. At each
5/// `chunk_size` step, the next delimiter in the data is searched and the
6/// chunk boundary is placed immediately after it (including the delimiter
7/// byte in the chunk).
8///
9/// The last chunk always extends to the end of `data`, regardless of
10/// whether a trailing delimiter exists.
11///
12/// Returns a `Vec` of `(start_offset, end_offset)` pairs. Offsets are
13/// absolute byte positions within `data`.
14pub fn find_chunk_boundaries(data: &[u8], chunk_size: usize, delimiter: u8) -> Vec<(usize, usize)> {
15    if data.is_empty() {
16        return Vec::new();
17    }
18
19    let len = data.len();
20    let step = chunk_size.max(1);
21    let estimate = (len / step) + 2;
22    let mut chunks = Vec::with_capacity(estimate);
23
24    let mut start = 0usize;
25
26    while start < len {
27        let mut end = start.saturating_add(step);
28
29        if end >= len {
30            end = len;
31        } else {
32            let remainder = &data[end..];
33            if let Some(rel_pos) = find_byte_swar(remainder, delimiter) {
34                end = end + rel_pos + 1;
35                if end > len {
36                    end = len;
37                }
38            } else {
39                end = len;
40            }
41        }
42
43        chunks.push((start, end));
44        start = end;
45    }
46
47    chunks
48}
49
50#[cfg(test)]
51mod differential_tests {
52    use super::{
53        find_byte_swar, find_chunk_boundaries, find_chunk_boundaries_pattern,
54        find_partition_boundaries, ChunkCursor, PatternChunkCursor,
55    };
56
57    const SINGLE_SEED: u64 = 0x5349_4e47_4c45_0001;
58    const CURSOR_SEED: u64 = 0x4355_5253_4f52_0002;
59    const PATTERN_SEED: u64 = 0x5041_5454_4552_0003;
60    const PATTERN_CURSOR_SEED: u64 = 0x5043_5552_534f_0004;
61    const SWAR_SEED: u64 = 0x5357_4152_0000_0005;
62    const PARTITION_SEED: u64 = 0x5041_5254_0000_0006;
63
64    #[derive(Clone, Copy)]
65    struct Lcg {
66        state: u64,
67    }
68
69    impl Lcg {
70        fn new(seed: u64) -> Self {
71            Self { state: seed }
72        }
73
74        fn next_u64(&mut self) -> u64 {
75            self.state = self
76                .state
77                .wrapping_mul(6_364_136_223_846_793_005)
78                .wrapping_add(1_442_695_040_888_963_407);
79            self.state
80        }
81
82        fn next_u8(&mut self) -> u8 {
83            self.next_u64() as u8
84        }
85
86        fn next_usize(&mut self, upper_exclusive: usize) -> usize {
87            if upper_exclusive == 0 {
88                0
89            } else {
90                (self.next_u64() % upper_exclusive as u64) as usize
91            }
92        }
93    }
94
95    fn scalar_single_byte_boundaries(
96        data: &[u8],
97        chunk_size: usize,
98        delimiter: u8,
99    ) -> Vec<(usize, usize)> {
100        let mut boundaries = Vec::new();
101        let step = chunk_size.max(1);
102        let mut start = 0;
103
104        while start < data.len() {
105            let target = start.saturating_add(step);
106            if target >= data.len() {
107                boundaries.push((start, data.len()));
108                break;
109            }
110
111            let mut end = target;
112            while end < data.len() {
113                if data[end] == delimiter {
114                    end += 1;
115                    break;
116                }
117                end += 1;
118            }
119            boundaries.push((start, end.min(data.len())));
120            start = end;
121        }
122
123        boundaries
124    }
125
126    fn scalar_byte_position(haystack: &[u8], delimiter: u8) -> Option<usize> {
127        let mut position = 0;
128        while position < haystack.len() {
129            if haystack[position] == delimiter {
130                return Some(position);
131            }
132            position += 1;
133        }
134        None
135    }
136
137    fn scalar_pattern_boundaries(
138        data: &[u8],
139        chunk_size: usize,
140        pattern: &[u8],
141    ) -> Vec<(usize, usize)> {
142        assert!(!pattern.is_empty());
143
144        let mut boundaries = Vec::new();
145        let step = chunk_size.max(1);
146        let mut start = 0;
147
148        while start < data.len() {
149            let target = start.saturating_add(step);
150            if target >= data.len() {
151                boundaries.push((start, data.len()));
152                break;
153            }
154
155            let mut candidate = target;
156            let mut end = data.len();
157            while candidate + pattern.len() <= data.len() {
158                let mut matches = true;
159                for offset in 0..pattern.len() {
160                    if data[candidate + offset] != pattern[offset] {
161                        matches = false;
162                        break;
163                    }
164                }
165                if matches {
166                    end = candidate + pattern.len();
167                    break;
168                }
169                candidate += 1;
170            }
171
172            boundaries.push((start, end));
173            start = end;
174        }
175
176        boundaries
177    }
178
179    fn scalar_partition_boundaries(
180        data: &[u8],
181        num_partitions: usize,
182        delimiter: u8,
183    ) -> Vec<(usize, usize)> {
184        if data.is_empty() || num_partitions == 0 {
185            return Vec::new();
186        }
187        if num_partitions == 1 {
188            return vec![(0, data.len())];
189        }
190
191        let mut cut_points = Vec::new();
192        let mut last_cut = 0;
193
194        for partition in 1..num_partitions {
195            let target = data.len() * partition / num_partitions;
196            if target <= last_cut {
197                continue;
198            }
199
200            let mut position = target;
201            while position < data.len() && data[position] != delimiter {
202                position += 1;
203            }
204
205            let cut = if position < data.len() {
206                position + 1
207            } else {
208                data.len()
209            };
210            cut_points.push(cut);
211            last_cut = cut;
212
213            if cut == data.len() {
214                break;
215            }
216        }
217
218        let mut partitions = Vec::with_capacity(cut_points.len() + 1);
219        let mut start = 0;
220        for end in cut_points {
221            if end > start {
222                partitions.push((start, end));
223            }
224            start = end;
225        }
226        if start < data.len() {
227            partitions.push((start, data.len()));
228        }
229        partitions
230    }
231
232    fn generated_single_case(seed: u64, case: usize) -> (Vec<u8>, usize, u8) {
233        const LENGTHS: &[usize] = &[0, 1, 2, 3, 7, 8, 9, 15, 16, 17, 31, 32, 63, 64, 127, 255];
234        let mut rng = Lcg::new(seed ^ (case as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15));
235        let len = if case % 3 == 0 {
236            LENGTHS[(case / 3) % LENGTHS.len()]
237        } else {
238            rng.next_usize(256)
239        };
240        let mut data = vec![0; len];
241        for byte in &mut data {
242            *byte = rng.next_u8();
243        }
244
245        let delimiter = match case % 8 {
246            0 => 0x00,
247            1 => 0xff,
248            2 => b'\n',
249            3 => 0x80,
250            _ => rng.next_u8(),
251        };
252        if case % 11 == 0 {
253            data.fill(delimiter);
254        } else if !data.is_empty() {
255            let len = data.len();
256            data[case % len] = delimiter;
257            if case % 3 == 1 && len > 1 {
258                data[(case * 7 + 1) % len] = delimiter;
259            }
260        }
261
262        let chunk_size = match case % 10 {
263            0 => 0,
264            1 => 1,
265            2 => 2,
266            3 => data.len(),
267            4 => data.len().saturating_add(1),
268            5 => usize::MAX,
269            _ => rng.next_usize(128),
270        };
271        (data, chunk_size, delimiter)
272    }
273
274    fn generated_pattern_case(seed: u64, case: usize) -> (Vec<u8>, usize, Vec<u8>) {
275        const DATA_LENGTHS: &[usize] = &[0, 1, 2, 3, 7, 8, 15, 16, 31, 32, 63, 64, 127];
276        const PATTERN_LENGTHS: &[usize] = &[1, 2, 3, 4, 5, 8, 16, 32, 64];
277        let mut rng = Lcg::new(seed ^ (case as u64).wrapping_mul(0xd6e8_feb8_6659_fd93));
278        let data_len = if case % 3 == 0 {
279            DATA_LENGTHS[(case / 3) % DATA_LENGTHS.len()]
280        } else {
281            rng.next_usize(192)
282        };
283        let pattern_len = PATTERN_LENGTHS[case % PATTERN_LENGTHS.len()];
284        let mut data = vec![0; data_len];
285        for byte in &mut data {
286            *byte = rng.next_u8();
287        }
288        let mut pattern = vec![0; pattern_len];
289        for byte in &mut pattern {
290            *byte = rng.next_u8();
291        }
292
293        let chunk_size = match case % 9 {
294            0 => 0,
295            1 => 1,
296            2 => 2,
297            3 => data_len,
298            4 => data_len.saturating_add(1),
299            5 => usize::MAX,
300            _ => rng.next_usize(96),
301        };
302
303        if case % 5 == 0 {
304            data.fill(b'a');
305            pattern.fill(b'a');
306            if pattern.len() > 1 {
307                *pattern.last_mut().unwrap() = b'b';
308            }
309        }
310
311        if data.len() >= pattern.len() {
312            let max_start = data.len() - pattern.len();
313            let start = match case % 4 {
314                0 => 0,
315                1 => max_start,
316                2 => chunk_size.max(1).min(max_start),
317                _ => rng.next_usize(max_start + 1),
318            };
319            data[start..start + pattern.len()].copy_from_slice(&pattern);
320        }
321
322        (data, chunk_size, pattern)
323    }
324
325    fn generated_partition_case(seed: u64, case: usize) -> (Vec<u8>, usize, u8) {
326        const LENGTHS: &[usize] = &[0, 1, 2, 3, 7, 8, 15, 16, 31, 32, 63, 64, 127, 255];
327        let mut rng = Lcg::new(seed ^ (case as u64).wrapping_mul(0xa409_3822_299f_31d0));
328        let len = if case % 4 == 0 {
329            LENGTHS[(case / 4) % LENGTHS.len()]
330        } else {
331            rng.next_usize(256)
332        };
333        let mut data = vec![0; len];
334        for byte in &mut data {
335            *byte = rng.next_u8();
336        }
337        let delimiter = match case % 7 {
338            0 => 0x00,
339            1 => 0xff,
340            2 => b'\n',
341            _ => rng.next_u8(),
342        };
343
344        match case % 10 {
345            0 => data.fill(delimiter),
346            1 => {}
347            _ if !data.is_empty() => {
348                let injections = 1 + case % 5;
349                let len = data.len();
350                for offset in 0..injections {
351                    data[(case * 13 + offset * 17) % len] = delimiter;
352                }
353            }
354            _ => {}
355        }
356
357        let num_partitions = match case % 9 {
358            0 => 0,
359            1 => 1,
360            2 => 2,
361            3 => 4,
362            4 => 8,
363            5 => 16,
364            6 => 64,
365            _ => 1 + rng.next_usize(128),
366        };
367        (data, num_partitions, delimiter)
368    }
369
370    fn cursor_ranges(data: &[u8], chunk_size: usize, delimiter: u8) -> Vec<(usize, usize)> {
371        let base = data.as_ptr() as usize;
372        let mut cursor = ChunkCursor::new(data, chunk_size, delimiter);
373        let mut ranges = Vec::new();
374        for chunk in cursor.by_ref() {
375            let start = chunk.as_ptr() as usize - base;
376            let end = start + chunk.len();
377            assert_eq!(&data[start..end], chunk);
378            ranges.push((start, end));
379        }
380        assert!(cursor.next().is_none());
381        assert!(cursor.is_empty());
382        assert_eq!(cursor.position(), data.len());
383        ranges
384    }
385
386    fn pattern_cursor_ranges(
387        data: &[u8],
388        chunk_size: usize,
389        pattern: &[u8],
390    ) -> Vec<(usize, usize)> {
391        let base = data.as_ptr() as usize;
392        let mut cursor = PatternChunkCursor::new(data, chunk_size, pattern);
393        let mut ranges = Vec::new();
394        for chunk in cursor.by_ref() {
395            let start = chunk.as_ptr() as usize - base;
396            let end = start + chunk.len();
397            assert_eq!(&data[start..end], chunk);
398            ranges.push((start, end));
399        }
400        assert!(cursor.next().is_none());
401        assert!(cursor.is_empty());
402        assert_eq!(cursor.position(), data.len());
403        ranges
404    }
405
406    fn assert_cover(data: &[u8], ranges: &[(usize, usize)]) {
407        if data.is_empty() {
408            assert!(ranges.is_empty());
409            return;
410        }
411        assert_eq!(ranges.first().unwrap().0, 0);
412        assert_eq!(ranges.last().unwrap().1, data.len());
413        let mut next_start = 0;
414        for &(start, end) in ranges {
415            assert_eq!(start, next_start);
416            assert!(end > start);
417            next_start = end;
418        }
419        assert_eq!(next_start, data.len());
420    }
421
422    fn assert_partition_invariants(
423        data: &[u8],
424        num_partitions: usize,
425        delimiter: u8,
426        partitions: &[(usize, usize)],
427    ) {
428        if data.is_empty() || num_partitions == 0 {
429            assert!(partitions.is_empty());
430            return;
431        }
432
433        assert!(!partitions.is_empty());
434        assert!(partitions.len() <= num_partitions);
435        assert_eq!(partitions.first().unwrap().0, 0);
436        assert_eq!(partitions.last().unwrap().1, data.len());
437
438        let mut previous_end = 0;
439        for (index, &(start, end)) in partitions.iter().enumerate() {
440            assert_eq!(start, previous_end, "gap or overlap at partition {index}");
441            assert!(end > start, "empty partition at index {index}");
442            if index + 1 < partitions.len() {
443                assert_eq!(data[end - 1], delimiter);
444            }
445            previous_end = end;
446        }
447        assert_eq!(previous_end, data.len());
448    }
449
450    #[test]
451    fn single_byte_oracle_matches_deterministic_corpus() {
452        let cases: &[(&[u8], usize, u8)] = &[
453            (b"", 4, b'\n'),
454            (b"x", 4, b'\n'),
455            (b"xxxx\n", 4, b'\n'),
456            (b"xx\nxx\n", 2, b'\n'),
457            (b"\n\n\n", 1, b'\n'),
458            (b"a\x00b\x00c", 2, 0),
459            (b"no delimiter", 3, b'\n'),
460        ];
461
462        for &(data, chunk_size, delimiter) in cases {
463            let expected = scalar_single_byte_boundaries(data, chunk_size, delimiter);
464            let actual = find_chunk_boundaries(data, chunk_size, delimiter);
465            assert_eq!(
466                actual, expected,
467                "mismatch for data={data:?}, chunk_size={chunk_size}, delimiter={delimiter:#04x}"
468            );
469            assert_cover(data, &expected);
470        }
471    }
472
473    #[test]
474    fn single_byte_oracle_matches_generated_cases() {
475        for case in 0..4096 {
476            let (data, chunk_size, delimiter) = generated_single_case(SINGLE_SEED, case);
477            let expected = scalar_single_byte_boundaries(&data, chunk_size, delimiter);
478            let actual = find_chunk_boundaries(&data, chunk_size, delimiter);
479            assert_eq!(
480                actual, expected,
481                "single-byte mismatch: seed={SINGLE_SEED:#018x}, case={case}, data={data:?}, chunk_size={chunk_size}, delimiter={delimiter:#04x}"
482            );
483            assert_cover(&data, &expected);
484        }
485    }
486
487    #[test]
488    fn cursor_ranges_match_single_byte_oracle() {
489        for case in 0..2048 {
490            let (data, chunk_size, delimiter) = generated_single_case(CURSOR_SEED, case);
491            let expected = scalar_single_byte_boundaries(&data, chunk_size, delimiter);
492            let eager = find_chunk_boundaries(&data, chunk_size, delimiter);
493            let cursor = cursor_ranges(&data, chunk_size, delimiter);
494            assert_eq!(
495                eager, expected,
496                "eager mismatch: seed={CURSOR_SEED:#018x}, case={case}"
497            );
498            assert_eq!(cursor, expected, "cursor mismatch: seed={CURSOR_SEED:#018x}, case={case}, data={data:?}, chunk_size={chunk_size}, delimiter={delimiter:#04x}");
499            assert_cover(&data, &cursor);
500            assert_eq!(cursor_ranges(&data, chunk_size, delimiter), cursor);
501        }
502    }
503
504    #[test]
505    fn pattern_oracle_matches_deterministic_fixtures() {
506        let cases: &[(&[u8], usize, &[u8])] = &[
507            (b"", 4, b"\r\n"),
508            (b"a\r\nb\r\nc", 4, b"\r\n"),
509            (b"a\x00\xff\x00b\x00\xff\x00c", 2, b"\x00\xff\x00"),
510            (b"aaaaaa", 1, b"aa"),
511            (b"prefixEND_RECORDsuffix", 3, b"END_RECORD"),
512            (b"no delimiter", 2, b"\r\n\r\n"),
513            (b"abc", 1, b"abcdef"),
514            (b"xx\r\n\r\nxx", 1, b"\r\n\r\n"),
515        ];
516
517        for &(data, chunk_size, pattern) in cases {
518            let expected = scalar_pattern_boundaries(data, chunk_size, pattern);
519            let actual = find_chunk_boundaries_pattern(data, chunk_size, pattern);
520            assert_eq!(
521                actual, expected,
522                "pattern mismatch for data={data:?}, chunk_size={chunk_size}, pattern={pattern:?}"
523            );
524            assert_cover(data, &expected);
525        }
526    }
527
528    #[test]
529    fn pattern_oracle_matches_generated_cases() {
530        for case in 0..4096 {
531            let (data, chunk_size, pattern) = generated_pattern_case(PATTERN_SEED, case);
532            let expected = scalar_pattern_boundaries(&data, chunk_size, &pattern);
533            let actual = find_chunk_boundaries_pattern(&data, chunk_size, &pattern);
534            assert_eq!(
535                actual, expected,
536                "pattern mismatch: seed={PATTERN_SEED:#018x}, case={case}, data={data:?}, chunk_size={chunk_size}, pattern={pattern:?}"
537            );
538            assert_cover(&data, &expected);
539        }
540    }
541
542    #[test]
543    fn pattern_cursor_ranges_match_pattern_oracle() {
544        for case in 0..2048 {
545            let (data, chunk_size, pattern) = generated_pattern_case(PATTERN_CURSOR_SEED, case);
546            let expected = scalar_pattern_boundaries(&data, chunk_size, &pattern);
547            let eager = find_chunk_boundaries_pattern(&data, chunk_size, &pattern);
548            let cursor = pattern_cursor_ranges(&data, chunk_size, &pattern);
549            assert_eq!(
550                eager, expected,
551                "eager pattern mismatch: seed={PATTERN_CURSOR_SEED:#018x}, case={case}"
552            );
553            assert_eq!(cursor, expected, "pattern cursor mismatch: seed={PATTERN_CURSOR_SEED:#018x}, case={case}, data={data:?}, chunk_size={chunk_size}, pattern={pattern:?}");
554            assert_cover(&data, &cursor);
555            assert_eq!(pattern_cursor_ranges(&data, chunk_size, &pattern), cursor);
556        }
557    }
558
559    #[test]
560    fn swar_matches_scalar_byte_search_across_offsets_and_lengths() {
561        const LENGTHS: &[usize] = &[
562            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127,
563        ];
564        let mut comparisons = 0usize;
565        for case in 0..16 {
566            let mut rng = Lcg::new(SWAR_SEED ^ case as u64);
567            for prefix in 0..8 {
568                for &len in LENGTHS {
569                    let mut backing = vec![0; prefix + len];
570                    for byte in &mut backing[prefix..] {
571                        *byte = rng.next_u8();
572                    }
573                    let haystack = &backing[prefix..];
574                    for delimiter in 0..=u8::MAX {
575                        assert_eq!(
576                            find_byte_swar(haystack, delimiter),
577                            scalar_byte_position(haystack, delimiter),
578                            "SWAR mismatch: seed={SWAR_SEED:#018x}, case={case}, prefix={prefix}, len={len}, delimiter={delimiter:#04x}, haystack={haystack:?}"
579                        );
580                        comparisons += 1;
581                    }
582                }
583            }
584        }
585        assert_eq!(comparisons, 16 * 8 * LENGTHS.len() * 256);
586    }
587
588    #[test]
589    fn partition_oracle_matches_deterministic_fixtures() {
590        let cases: &[(&[u8], usize, u8)] = &[
591            (b"", 4, b'\n'),
592            (b"x", 1, b'\n'),
593            (b"no delimiter", 8, b'\n'),
594            (b"a\n\n\nb\n", 8, b'\n'),
595            (b"aa\nbbbb\ncccccccccccc\ndd\n", 4, b'\n'),
596            (b"aaaa\x00bbbb\x00cccc", 2, 0),
597            (b"123456789", 0, b'\n'),
598            (b"123456789", 1, b'\n'),
599            (b"123456789", 64, b'\n'),
600        ];
601
602        for &(data, num_partitions, delimiter) in cases {
603            let expected = scalar_partition_boundaries(data, num_partitions, delimiter);
604            let actual = find_partition_boundaries(data, num_partitions, delimiter);
605            assert_eq!(
606                actual, expected,
607                "partition mismatch for data={data:?}, n={num_partitions}, delimiter={delimiter:#04x}"
608            );
609            assert_partition_invariants(data, num_partitions, delimiter, &actual);
610        }
611
612        let mut giant = vec![b'x'; 10_000];
613        giant.extend_from_slice(b"\nsmall\nrecords\n");
614        let expected = scalar_partition_boundaries(&giant, 64, b'\n');
615        let actual = find_partition_boundaries(&giant, 64, b'\n');
616        assert_eq!(actual, expected);
617        assert_partition_invariants(&giant, 64, b'\n', &actual);
618    }
619
620    #[test]
621    fn partition_oracle_matches_generated_cases() {
622        for case in 0..4096 {
623            let (data, num_partitions, delimiter) = generated_partition_case(PARTITION_SEED, case);
624            let expected = scalar_partition_boundaries(&data, num_partitions, delimiter);
625            let actual = find_partition_boundaries(&data, num_partitions, delimiter);
626            assert_eq!(
627                actual, expected,
628                "partition mismatch: seed={PARTITION_SEED:#018x}, case={case}, data={data:?}, n={num_partitions}, delimiter={delimiter:#04x}"
629            );
630            assert_partition_invariants(&data, num_partitions, delimiter, &actual);
631            assert_eq!(
632                find_partition_boundaries(&data, num_partitions, delimiter),
633                actual
634            );
635        }
636    }
637
638    #[test]
639    fn partition_request_above_file_len_matches_file_len_oracle() {
640        for case in 0..4096 {
641            let (data, _, delimiter) = generated_partition_case(PARTITION_SEED, case);
642            if data.is_empty() {
643                continue;
644            }
645
646            let expected = find_partition_boundaries(&data, data.len(), delimiter);
647            for requested in [data.len() + 1, usize::MAX] {
648                assert_eq!(
649                    find_partition_boundaries(&data, requested, delimiter),
650                    expected,
651                    "request above file length changed output: seed={PARTITION_SEED:#018x}, case={case}, data_len={}, requested={requested}, delimiter={delimiter:#04x}",
652                    data.len()
653                );
654            }
655        }
656    }
657}
658
659/// A lazy, streaming cursor that yields delimiter-aligned chunks
660/// sequentially without pre-computing all boundaries.
661///
662/// Each call to [`next`](ChunkCursor::next) produces a single chunk using
663/// the same boundary semantics as [`find_chunk_boundaries`], reusing the
664/// SWAR byte search internally. The cursor advances its internal position
665/// and yields `&[u8]` slices directly referencing the input data.
666///
667/// # Memory footprint
668///
669/// O(1) state: a single struct (~40 bytes on 64-bit) regardless of file
670/// size, compared to O(number_of_chunks) for the eager `Vec<(usize,usize)>`
671/// approach (16 bytes per chunk).
672///
673/// # Example
674///
675/// ```
676/// use mmap_chunker_core::scanner::ChunkCursor;
677///
678/// let data = b"aaa\nbbb\nccc\nddd\n";
679/// let chunks: Vec<&[u8]> = ChunkCursor::new(data, 4, b'\n').collect();
680/// assert_eq!(chunks, vec![b"aaa\nbbb\n" as &[u8], b"ccc\nddd\n" as &[u8]]);
681/// ```
682#[derive(Debug, Clone)]
683pub struct ChunkCursor<'a> {
684    data: &'a [u8],
685    chunk_size: usize,
686    delimiter: u8,
687    position: usize,
688}
689
690impl<'a> ChunkCursor<'a> {
691    /// Create a new cursor over `data` with the given approximate
692    /// `chunk_size` and single-byte `delimiter`.
693    ///
694    /// Chunk boundaries are placed at or after each `chunk_size`
695    /// interval, snapped to the next occurrence of `delimiter`.
696    /// The last chunk extends to EOF. Empty input produces an
697    /// exhausted cursor (no items yielded).
698    #[inline]
699    pub fn new(data: &'a [u8], chunk_size: usize, delimiter: u8) -> Self {
700        Self {
701            data,
702            chunk_size,
703            delimiter,
704            position: 0,
705        }
706    }
707
708    /// Returns the current position (start of the next chunk).
709    #[inline]
710    pub fn position(&self) -> usize {
711        self.position
712    }
713
714    /// Returns the total number of bytes.
715    #[inline]
716    pub fn len(&self) -> usize {
717        self.data.len()
718    }
719
720    /// Returns `true` if all chunks have been consumed.
721    #[inline]
722    pub fn is_empty(&self) -> bool {
723        self.position >= self.data.len()
724    }
725}
726
727impl<'a> Iterator for ChunkCursor<'a> {
728    type Item = &'a [u8];
729
730    fn next(&mut self) -> Option<&'a [u8]> {
731        let len = self.data.len();
732        if self.position >= len {
733            return None;
734        }
735
736        let step = self.chunk_size.max(1);
737        let target = self.position.saturating_add(step);
738        let end = if target >= len {
739            len
740        } else {
741            let remainder = &self.data[target..];
742            match find_byte_swar(remainder, self.delimiter) {
743                Some(rel_pos) => (target + rel_pos + 1).min(len),
744                None => len,
745            }
746        };
747
748        let chunk = &self.data[self.position..end];
749        self.position = end;
750        Some(chunk)
751    }
752
753    fn size_hint(&self) -> (usize, Option<usize>) {
754        let len = self.data.len();
755        if self.position >= len {
756            return (0, Some(0));
757        }
758        let remaining = len - self.position;
759        (1, Some(remaining))
760    }
761}
762
763/// Search `haystack` for the first occurrence of the multi-byte `pattern`.
764///
765/// Uses first-byte SWAR to find candidates, then verifies with
766/// [`starts_with`]. For single-byte patterns, this is equivalent to
767/// [`find_byte_swar`]. For longer patterns, each candidate position
768/// resynchronizes the search after a false-positive first-byte match.
769///
770/// Time complexity: O(n + m) typical, O(n*m) pathological (repeated
771/// prefix). No unsafe. No dependencies. MSRV 1.77.
772fn find_pattern_in_slice(haystack: &[u8], pattern: &[u8]) -> Option<usize> {
773    let plen = pattern.len();
774    if plen == 0 || haystack.len() < plen {
775        return None;
776    }
777    if plen == 1 {
778        return find_byte_swar(haystack, pattern[0]);
779    }
780
781    let first_byte = pattern[0];
782    let hlen = haystack.len();
783    let max_search = hlen - plen;
784    let mut search_start = 0;
785
786    while search_start <= max_search {
787        let remainder = &haystack[search_start..];
788        let rel_pos = find_byte_swar(remainder, first_byte)?;
789        let pos = search_start + rel_pos;
790        if pos > max_search {
791            return None;
792        }
793        if haystack[pos..].starts_with(pattern) {
794            return Some(pos);
795        }
796        search_start = pos + 1;
797    }
798    None
799}
800
801/// Find chunk boundaries in `data` using a multi-byte `delimiter`.
802///
803/// Same semantics as [`find_chunk_boundaries`] but the delimiter can be
804/// multiple bytes (e.g., `b"\r\n"` for CRLF, `b"\r\n\r\n"` for HTTP
805/// headers). Chunks are placed immediately after the complete delimiter.
806///
807/// When `delimiter.len() == 1`, this delegates to the single-byte SWAR
808/// fast path and produces identical output.
809///
810/// # Panics
811///
812/// Panics if `delimiter` is empty.
813pub fn find_chunk_boundaries_pattern(
814    data: &[u8],
815    chunk_size: usize,
816    delimiter: &[u8],
817) -> Vec<(usize, usize)> {
818    assert!(!delimiter.is_empty(), "delimiter must not be empty");
819    if data.is_empty() {
820        return Vec::new();
821    }
822
823    let dlen = delimiter.len();
824    let len = data.len();
825    let step = chunk_size.max(1);
826    let estimate = (len / step) + 2;
827    let mut chunks = Vec::with_capacity(estimate);
828
829    let mut start = 0usize;
830
831    while start < len {
832        let mut end = start.saturating_add(step);
833
834        if end >= len {
835            end = len;
836        } else {
837            let remainder = &data[end..];
838            if let Some(rel_pos) = find_pattern_in_slice(remainder, delimiter) {
839                end = end + rel_pos + dlen;
840                if end > len {
841                    end = len;
842                }
843            } else {
844                end = len;
845            }
846        }
847
848        chunks.push((start, end));
849        start = end;
850    }
851
852    chunks
853}
854
855/// A lazy, streaming cursor for multi-byte delimiter chunking.
856///
857/// Like [`ChunkCursor`] but accepts a slice pattern as the delimiter.
858/// Each call to [`next`](PatternChunkCursor::next) yields a chunk aligned
859/// after the complete multi-byte delimiter.
860///
861/// Single-byte patterns are handled via the same SWAR fast path as
862/// [`ChunkCursor`]. Multi-byte patterns use first-byte SWAR candidate
863/// search with [`starts_with`] verification.
864///
865/// # Panics
866///
867/// Panics if `delimiter` is empty.
868///
869/// # Example
870///
871/// ```
872/// use mmap_chunker_core::PatternChunkCursor;
873///
874/// let data = b"a\r\nb\r\nc\r\n";
875/// let chunks: Vec<&[u8]> = PatternChunkCursor::new(data, 4, b"\r\n").collect();
876/// assert_eq!(chunks, vec![b"a\r\nb\r\n" as &[u8], b"c\r\n" as &[u8]]);
877/// ```
878#[derive(Debug, Clone)]
879pub struct PatternChunkCursor<'a, 'p> {
880    data: &'a [u8],
881    chunk_size: usize,
882    delimiter: &'p [u8],
883    position: usize,
884}
885
886impl<'a, 'p> PatternChunkCursor<'a, 'p> {
887    /// Create a new pattern cursor with the given multi-byte `delimiter`.
888    ///
889    /// # Panics
890    ///
891    /// Panics if `delimiter` is empty.
892    #[inline]
893    pub fn new(data: &'a [u8], chunk_size: usize, delimiter: &'p [u8]) -> Self {
894        assert!(!delimiter.is_empty(), "delimiter must not be empty");
895        Self {
896            data,
897            chunk_size,
898            delimiter,
899            position: 0,
900        }
901    }
902
903    #[inline]
904    pub fn position(&self) -> usize {
905        self.position
906    }
907
908    #[inline]
909    pub fn len(&self) -> usize {
910        self.data.len()
911    }
912
913    /// Returns `true` if all chunks have been consumed.
914    #[inline]
915    pub fn is_empty(&self) -> bool {
916        self.position >= self.data.len()
917    }
918}
919
920impl<'a, 'p> Iterator for PatternChunkCursor<'a, 'p> {
921    type Item = &'a [u8];
922
923    fn next(&mut self) -> Option<&'a [u8]> {
924        let len = self.data.len();
925        if self.position >= len {
926            return None;
927        }
928
929        let dlen = self.delimiter.len();
930        let step = self.chunk_size.max(1);
931        let target = self.position.saturating_add(step);
932        let end = if target >= len {
933            len
934        } else {
935            let remainder = &self.data[target..];
936            match find_pattern_in_slice(remainder, self.delimiter) {
937                Some(rel_pos) => (target + rel_pos + dlen).min(len),
938                None => len,
939            }
940        };
941
942        let chunk = &self.data[self.position..end];
943        self.position = end;
944        Some(chunk)
945    }
946
947    fn size_hint(&self) -> (usize, Option<usize>) {
948        let len = self.data.len();
949        if self.position >= len {
950            return (0, Some(0));
951        }
952        let remaining = len - self.position;
953        (1, Some(remaining))
954    }
955}
956
957/// Safe SWAR (SIMD Within A Register) byte search.
958///
959/// Scans `haystack` for the first occurrence of `delimiter`. Processes
960/// 8 bytes per iteration using word-at-a-time bit manipulation, with a
961/// scalar prefix for alignment and a scalar tail for the final <8 bytes.
962///
963/// No unsafe. No dependencies. MSRV 1.77.
964pub(crate) fn find_byte_swar(haystack: &[u8], delimiter: u8) -> Option<usize> {
965    let len = haystack.len();
966    if len == 0 {
967        return None;
968    }
969
970    let pattern = (delimiter as u64).wrapping_mul(0x0101010101010101u64);
971    let lo = 0x0101010101010101u64;
972    let hi = 0x8080808080808080u64;
973
974    let mut i = 0usize;
975
976    // Phase 1: scalar prefix to reach 8-byte alignment
977    let ptr = haystack.as_ptr() as usize;
978    let align = ptr % 8;
979    if align != 0 {
980        let prefix_end = (8 - align).min(len);
981        while i < prefix_end {
982            if haystack[i] == delimiter {
983                return Some(i);
984            }
985            i += 1;
986        }
987    }
988
989    // Phase 2: SWAR main loop (8-byte reads)
990    while i + 8 <= len {
991        let chunk: [u8; 8] = haystack[i..i + 8].try_into().unwrap();
992        let word = u64::from_ne_bytes(chunk);
993        let xored = word ^ pattern;
994
995        let has_zero = xored.wrapping_sub(lo) & !xored & hi;
996        if has_zero != 0 {
997            return Some(i + (has_zero.trailing_zeros() / 8) as usize);
998        }
999        i += 8;
1000    }
1001
1002    // Phase 3: scalar tail (< 8 bytes)
1003    while i < len {
1004        if haystack[i] == delimiter {
1005            return Some(i);
1006        }
1007        i += 1;
1008    }
1009
1010    None
1011}
1012
1013/// Number of fixed-size chunks a file of `file_len` bytes would produce
1014/// with the given `chunk_size`.
1015///
1016/// `chunk_size` of 0 is clamped to 1, consistent with the delimiter scanner.
1017#[inline]
1018pub fn fixed_chunk_count(file_len: usize, chunk_size: usize) -> usize {
1019    if file_len == 0 {
1020        return 0;
1021    }
1022    file_len.div_ceil(chunk_size.max(1))
1023}
1024
1025/// Compute the (start, end) boundaries for the `index`-th fixed-size chunk.
1026///
1027/// Returns `None` if `index >= fixed_chunk_count(file_len, chunk_size)`.
1028///
1029/// Overflow-safe: uses saturating arithmetic with a `min(file_len)` clamp.
1030/// `chunk_size` of 0 is clamped to 1.
1031#[inline]
1032pub fn fixed_chunk_bounds(
1033    file_len: usize,
1034    chunk_size: usize,
1035    index: usize,
1036) -> Option<(usize, usize)> {
1037    let effective_size = chunk_size.max(1);
1038    if file_len == 0 {
1039        return None;
1040    }
1041    let count = file_len.div_ceil(effective_size);
1042    if index >= count {
1043        return None;
1044    }
1045    let start = index.saturating_mul(effective_size);
1046
1047    let raw_end = start.saturating_add(effective_size);
1048    let end = if raw_end > file_len {
1049        file_len
1050    } else {
1051        raw_end
1052    };
1053
1054    Some((start, end))
1055}
1056
1057/// Compute N record-aligned partition boundaries covering `data`.
1058///
1059/// For each partition boundary `i = 1..N-1`, computes an ideal absolute
1060/// target position at `floor(data.len() * i / N)`, then searches forward
1061/// to the next occurrence of `delimiter`. Each boundary is placed
1062/// immediately after the delimiter byte (delimiter included in the
1063/// preceding partition).
1064///
1065/// If a single record spans multiple ideal target positions, those
1066/// boundaries collapse to the end of that record (deduplication). The
1067/// effective number of partitions may therefore be less than
1068/// `num_partitions`; the returned `Vec` length reflects the actual
1069/// partition count.
1070///
1071/// # Properties
1072///
1073/// - `first.start == 0`, `last.end == data.len()` — complete coverage
1074/// - No gaps, no overlaps — adjacent partitions are contiguous
1075/// - Boundaries respect record integrity — every non-final partition
1076///   ends immediately after a delimiter (or at EOF for the final one)
1077/// - Deterministic — same input always produces same output
1078/// - Partition sizes approximate `data.len() / actual_count`
1079/// - Maximum boundary deviation from ideal bounded by max record size
1080/// - `O(N)` metadata, bounded byte scanning (≤ data.len() total)
1081///
1082/// # Edge cases
1083///
1084/// | Case | Behavior |
1085/// |------|----------|
1086/// | `data` is empty | Returns empty `Vec` |
1087/// | `num_partitions == 0` | Returns empty `Vec` |
1088/// | `num_partitions == 1` | Returns `[(0, data.len())]` |
1089/// | No delimiter in entire file | Returns `[(0, data.len())]` |
1090/// | Fewer records than `N` | Produces ≤ record_count partitions |
1091/// | `num_partitions > data.len()` | Equivalent to requesting `data.len()` |
1092/// | Giant record spanning multiple targets | Boundaries collapse, no record split |
1093pub fn find_partition_boundaries(
1094    data: &[u8],
1095    num_partitions: usize,
1096    delimiter: u8,
1097) -> Vec<(usize, usize)> {
1098    let file_len = data.len();
1099    if file_len == 0 || num_partitions == 0 {
1100        return Vec::new();
1101    }
1102    if num_partitions == 1 {
1103        return vec![(0, file_len)];
1104    }
1105
1106    // A non-empty byte slice can contain at most `file_len` non-empty
1107    // partitions. For larger requests, the original absolute-target
1108    // algorithm produces the same ordered set of useful target offsets as
1109    // requesting exactly `file_len` partitions. Cap the request before
1110    // iterating or allocating so an untrusted count cannot cause a capacity
1111    // overflow or an impractically long run on a small input.
1112    let n = num_partitions.min(file_len);
1113
1114    let mut boundaries = Vec::new();
1115    let mut last_boundary: usize = 0;
1116
1117    for i in 1..n {
1118        // Overflow-safe: use u128 intermediate for multiplication.
1119        let target = ((file_len as u128) * (i as u128) / (n as u128)) as usize;
1120        if target <= last_boundary {
1121            continue;
1122        }
1123
1124        let remainder = &data[target..];
1125        match find_byte_swar(remainder, delimiter) {
1126            Some(rel_pos) => {
1127                let boundary = target + rel_pos + 1;
1128                let boundary = boundary.min(file_len);
1129                boundaries.push(boundary);
1130                last_boundary = boundary;
1131            }
1132            None => {
1133                boundaries.push(file_len);
1134                break;
1135            }
1136        }
1137    }
1138
1139    let effective_n = boundaries.len() + 1;
1140    let mut partitions = Vec::with_capacity(effective_n);
1141
1142    let mut prev = 0usize;
1143    for &b in &boundaries {
1144        if b > prev {
1145            partitions.push((prev, b));
1146        }
1147        prev = b;
1148    }
1149
1150    if prev < file_len {
1151        partitions.push((prev, file_len));
1152    }
1153
1154    partitions
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159    use super::*;
1160
1161    #[test]
1162    fn test_empty_input() {
1163        assert_eq!(find_chunk_boundaries(b"", 1024, b'\n'), vec![]);
1164    }
1165
1166    #[test]
1167    fn test_smaller_than_chunk() {
1168        let data = b"hello\nworld\n";
1169        let chunks = find_chunk_boundaries(data, 1024, b'\n');
1170        assert_eq!(chunks, vec![(0, 12)]);
1171    }
1172
1173    #[test]
1174    fn test_fixed_lines() {
1175        let data = b"aaa\nbbb\nccc\nddd\neee\n";
1176        let chunks = find_chunk_boundaries(data, 6, b'\n');
1177        assert_eq!(chunks.len(), 3);
1178        assert_eq!(&data[chunks[0].0..chunks[0].1], b"aaa\nbbb\n");
1179        assert_eq!(&data[chunks[1].0..chunks[1].1], b"ccc\nddd\n");
1180        assert_eq!(&data[chunks[2].0..chunks[2].1], b"eee\n");
1181    }
1182
1183    #[test]
1184    fn test_no_delimiter_in_large_remainder() {
1185        let data = b"aaa\nbbb\ncccccccccccccccc";
1186        let chunks = find_chunk_boundaries(data, 4, b'\n');
1187        assert_eq!(chunks.len(), 2);
1188        assert_eq!(chunks[0], (0, 4 + 3 + 1)); // 0..8 = "aaa\nbbb\n"
1189        assert_eq!(chunks[1], (8, data.len()));
1190    }
1191
1192    #[test]
1193    fn test_no_delimiter() {
1194        let data = b"no_newlines_here";
1195        let chunks = find_chunk_boundaries(data, 5, b'\n');
1196        assert_eq!(chunks, vec![(0, data.len())]);
1197    }
1198
1199    #[test]
1200    fn test_sequential_coverage() {
1201        let data = b"line1\nline2\nline3\nline4\nline5\n";
1202        let chunks = find_chunk_boundaries(data, 10, b'\n');
1203        let mut pos = 0;
1204        for (start, end) in &chunks {
1205            assert_eq!(*start, pos);
1206            pos = *end;
1207        }
1208        assert_eq!(pos, data.len());
1209    }
1210
1211    #[test]
1212    fn test_chunk_size_zero_clamps_to_one() {
1213        let data = b"abc\n";
1214        let chunks = find_chunk_boundaries(data, 0, b'\n');
1215        assert!(!chunks.is_empty());
1216    }
1217
1218    #[test]
1219    fn test_chunk_size_one() {
1220        let data = b"a\nb\nc\n";
1221        let chunks = find_chunk_boundaries(data, 1, b'\n');
1222        let mut pos = 0;
1223        for (start, end) in &chunks {
1224            assert_eq!(*start, pos);
1225            assert!(end > start);
1226            pos = *end;
1227        }
1228        assert_eq!(pos, data.len());
1229    }
1230
1231    #[test]
1232    fn test_no_trailing_newline() {
1233        let data = b"hello\nworld";
1234        let chunks = find_chunk_boundaries(data, 100, b'\n');
1235        assert_eq!(chunks, vec![(0, 11)]);
1236    }
1237
1238    #[test]
1239    fn test_only_newlines() {
1240        let data = b"\n\n\n";
1241        let chunks = find_chunk_boundaries(data, 1, b'\n');
1242        let mut pos = 0;
1243        for (start, end) in &chunks {
1244            assert_eq!(*start, pos);
1245            pos = *end;
1246        }
1247        assert_eq!(pos, data.len());
1248    }
1249
1250    #[test]
1251    fn test_consecutive_delimiters() {
1252        let data = b"line1\n\n\nline2\n";
1253        let chunks = find_chunk_boundaries(data, 6, b'\n');
1254        let mut pos = 0;
1255        for (start, end) in &chunks {
1256            assert_eq!(*start, pos);
1257            pos = *end;
1258        }
1259        assert_eq!(pos, data.len());
1260    }
1261
1262    #[test]
1263    fn test_record_larger_than_chunk() {
1264        let data = b"short\nverylonglinewithnoddelimiteratall\nshort\n";
1265        let chunks = find_chunk_boundaries(data, 6, b'\n');
1266        let mut pos = 0;
1267        for (start, end) in &chunks {
1268            assert_eq!(*start, pos);
1269            pos = *end;
1270        }
1271        assert_eq!(pos, data.len());
1272    }
1273
1274    #[test]
1275    fn test_binary_with_nul() {
1276        let data = b"prefix\x00suffix\n";
1277        let chunks = find_chunk_boundaries(data, 100, b'\n');
1278        assert_eq!(chunks, vec![(0, 14)]);
1279    }
1280
1281    #[test]
1282    fn test_chunk_size_larger_than_data() {
1283        let data = b"tiny\n";
1284        let chunks = find_chunk_boundaries(data, 1_000_000, b'\n');
1285        assert_eq!(chunks, vec![(0, 5)]);
1286    }
1287
1288    #[test]
1289    fn test_chunk_size_exact_multiple() {
1290        let data = b"xxxx\n";
1291        let chunks = find_chunk_boundaries(data, 5, b'\n');
1292        assert_eq!(chunks, vec![(0, 5)]);
1293    }
1294
1295    #[test]
1296    fn test_repeated_scan_same_size() {
1297        let data = b"a\nb\nc\n";
1298        let chunks1 = find_chunk_boundaries(data, 2, b'\n');
1299        let chunks2 = find_chunk_boundaries(data, 2, b'\n');
1300        assert_eq!(chunks1, chunks2);
1301    }
1302
1303    #[test]
1304    fn test_repeated_scan_different_size() {
1305        let data = b"aaa\nbbb\nccc\nddd\n";
1306        let chunks1 = find_chunk_boundaries(data, 4, b'\n');
1307        let chunks2 = find_chunk_boundaries(data, 8, b'\n');
1308        assert_ne!(chunks1, chunks2);
1309        let sum1: usize = chunks1.iter().map(|(s, e)| e - s).sum();
1310        let sum2: usize = chunks2.iter().map(|(s, e)| e - s).sum();
1311        assert_eq!(sum1, data.len());
1312        assert_eq!(sum2, data.len());
1313    }
1314
1315    #[test]
1316    fn test_one_byte_file() {
1317        let data = b"x";
1318        let chunks = find_chunk_boundaries(data, 1024, b'\n');
1319        assert_eq!(chunks, vec![(0, 1)]);
1320    }
1321
1322    #[test]
1323    fn test_one_byte_newline() {
1324        let data = b"\n";
1325        let chunks = find_chunk_boundaries(data, 1024, b'\n');
1326        assert_eq!(chunks, vec![(0, 1)]);
1327    }
1328
1329    #[test]
1330    fn property_concatenation_equals_input() {
1331        let cases: &[(&[u8], usize, u8)] = &[
1332            (b"hello\nworld\n", 4, b'\n'),
1333            (b"a,b,c,d", 2, b','),
1334            (b"one\ttwo\tthree", 4, b'\t'),
1335            (b"a|b|c|d|e|f", 3, b'|'),
1336            (b"x\x00y\x00z", 2, b'\x00'),
1337            (b"single", 1024, b'\n'),
1338            (b"\n\n\n\n\n", 1, b'\n'),
1339            (b"", 1024, b'\n'),
1340            (b"\n", 1, b'\n'),
1341            (b"a", 1024, b'\n'),
1342        ];
1343        for &(data, chunk_size, delim) in cases {
1344            let chunks = find_chunk_boundaries(data, chunk_size, delim);
1345            let total: usize = chunks.iter().map(|(s, e)| e - s).sum();
1346            assert_eq!(total, data.len(), "concatenation property failed");
1347        }
1348    }
1349
1350    #[test]
1351    fn property_no_gaps() {
1352        let cases: &[(&[u8], usize, u8)] = &[
1353            (b"a\nb\nc\n", 2, b'\n'),
1354            (b"a,b,c,d,e", 1, b','),
1355            (b"a\tb\tc\t", 2, b'\t'),
1356        ];
1357        for &(data, chunk_size, delim) in cases {
1358            let chunks = find_chunk_boundaries(data, chunk_size, delim);
1359            if chunks.is_empty() {
1360                continue;
1361            }
1362            assert_eq!(chunks[0].0, 0, "first chunk must start at 0");
1363            for i in 1..chunks.len() {
1364                assert_eq!(
1365                    chunks[i].0,
1366                    chunks[i - 1].1,
1367                    "gap at chunk {}->{}",
1368                    i - 1,
1369                    i
1370                );
1371            }
1372            assert_eq!(
1373                chunks.last().unwrap().1,
1374                data.len(),
1375                "last chunk must end at EOF"
1376            );
1377        }
1378    }
1379
1380    #[test]
1381    fn property_determinism() {
1382        let data = b"line1\nline2\nline3\nline4\nline5\n";
1383        let chunks1 = find_chunk_boundaries(data, 10, b'\n');
1384        let chunks2 = find_chunk_boundaries(data, 10, b'\n');
1385        assert_eq!(chunks1, chunks2);
1386        let chunks3 = find_chunk_boundaries(data, 10, b'\n');
1387        assert_eq!(chunks1, chunks3);
1388    }
1389
1390    #[test]
1391    fn property_monotonic_offsets() {
1392        let data = b"x\nxx\nxxx\nxxxx\nxxxxx\n";
1393        let chunks = find_chunk_boundaries(data, 1, b'\n');
1394        let mut last_end = 0usize;
1395        for (start, end) in &chunks {
1396            assert!(*start >= last_end, "offsets must be monotonic");
1397            assert!(*end > *start, "chunk must be non-empty");
1398            last_end = *end;
1399        }
1400    }
1401
1402    #[test]
1403    fn test_alternative_delimiters() {
1404        assert_eq!(
1405            find_chunk_boundaries(b"a,b,c,d,e", 2, b','),
1406            vec![(0, 4), (4, 8), (8, 9)]
1407        );
1408        assert_eq!(
1409            find_chunk_boundaries(b"one\ttwo\tthree", 4, b'\t'),
1410            vec![(0, 8), (8, 13)]
1411        );
1412        assert_eq!(
1413            find_chunk_boundaries(b"a|b|c|d|e|f", 3, b'|'),
1414            vec![(0, 4), (4, 8), (8, 11)]
1415        );
1416        assert_eq!(
1417            find_chunk_boundaries(b"x\x00y\x00z", 2, b'\x00'),
1418            vec![(0, 4), (4, 5)]
1419        );
1420    }
1421
1422    // ── ChunkCursor tests ──────────────────────────────────────────────
1423
1424    /// Verify cursor produces identical chunks as eager scanner.
1425    fn cursor_equals_eager(data: &[u8], chunk_size: usize, delimiter: u8) {
1426        let eager = find_chunk_boundaries(data, chunk_size, delimiter);
1427        let cursor: Vec<&[u8]> = ChunkCursor::new(data, chunk_size, delimiter).collect();
1428        let lazy_ranges: Vec<(usize, usize)> = cursor
1429            .iter()
1430            .scan(0usize, |pos, &chunk| {
1431                let start = *pos;
1432                *pos += chunk.len();
1433                Some((start, *pos))
1434            })
1435            .collect();
1436        assert_eq!(
1437            lazy_ranges, eager,
1438            "cursor mismatch: chunk_size={chunk_size}, delim={delimiter:#04x}"
1439        );
1440    }
1441
1442    #[test]
1443    fn cursor_empty_input() {
1444        assert_eq!(
1445            ChunkCursor::new(b"", 1024, b'\n').collect::<Vec<_>>(),
1446            Vec::<&[u8]>::new()
1447        );
1448    }
1449
1450    #[test]
1451    fn cursor_one_byte_file() {
1452        cursor_equals_eager(b"x", 1024, b'\n');
1453    }
1454
1455    #[test]
1456    fn cursor_one_byte_delimiter() {
1457        cursor_equals_eager(b"\n", 1024, b'\n');
1458    }
1459
1460    #[test]
1461    fn cursor_delimiter_only_file() {
1462        cursor_equals_eager(b"\n\n\n", 1, b'\n');
1463    }
1464
1465    #[test]
1466    fn cursor_no_delimiter() {
1467        cursor_equals_eager(b"no_newlines_here", 5, b'\n');
1468    }
1469
1470    #[test]
1471    fn cursor_delimiter_exactly_at_target() {
1472        cursor_equals_eager(b"xxxx\n", 5, b'\n');
1473    }
1474
1475    #[test]
1476    fn cursor_delimiter_after_target() {
1477        cursor_equals_eager(b"xxxyy\n", 3, b'\n');
1478    }
1479
1480    #[test]
1481    fn cursor_multiple_consecutive_delimiters() {
1482        cursor_equals_eager(b"line1\n\n\nline2\n", 6, b'\n');
1483    }
1484
1485    #[test]
1486    fn cursor_nul_delimiter() {
1487        cursor_equals_eager(b"prefix\x00suffix\n", 100, b'\n');
1488    }
1489
1490    #[test]
1491    fn cursor_giant_record() {
1492        cursor_equals_eager(b"tiny\nverylongrecordwithnobreaksanywhere\nend\n", 6, b'\n');
1493    }
1494
1495    #[test]
1496    fn cursor_no_trailing_delimiter() {
1497        cursor_equals_eager(b"hello\nworld", 100, b'\n');
1498    }
1499
1500    #[test]
1501    fn cursor_chunk_size_zero() {
1502        cursor_equals_eager(b"abc\n", 0, b'\n');
1503    }
1504
1505    #[test]
1506    fn cursor_chunk_size_one() {
1507        cursor_equals_eager(b"a\nb\nc\n", 1, b'\n');
1508    }
1509
1510    #[test]
1511    fn cursor_chunk_size_larger_than_data() {
1512        cursor_equals_eager(b"tiny\n", 1_000_000, b'\n');
1513    }
1514
1515    #[test]
1516    fn cursor_chunk_size_equals_len() {
1517        cursor_equals_eager(b"aaaa\n", 5, b'\n');
1518    }
1519
1520    #[test]
1521    fn cursor_different_delimiters() {
1522        cursor_equals_eager(b"a,b,c,d,e", 2, b',');
1523        cursor_equals_eager(b"one\ttwo\tthree", 4, b'\t');
1524        cursor_equals_eager(b"a|b|c|d|e|f", 3, b'|');
1525        cursor_equals_eager(b"x\x00y\x00z", 2, b'\x00');
1526    }
1527
1528    #[test]
1529    fn cursor_binary_input() {
1530        let data: Vec<u8> = (0u8..=255).collect();
1531        cursor_equals_eager(&data, 32, b'\n');
1532        cursor_equals_eager(&data, 32, 0x00);
1533        cursor_equals_eager(&data, 32, 0xff);
1534    }
1535
1536    #[test]
1537    fn cursor_repeated_iteration_new_cursor() {
1538        let data = b"a\nb\nc\n";
1539        let first: Vec<&[u8]> = ChunkCursor::new(data, 2, b'\n').collect();
1540        let second: Vec<&[u8]> = ChunkCursor::new(data, 2, b'\n').collect();
1541        assert_eq!(first, second);
1542    }
1543
1544    #[test]
1545    fn cursor_fixed_lines_equivalence() {
1546        let data = b"aaa\nbbb\nccc\nddd\neee\n";
1547        cursor_equals_eager(data, 6, b'\n');
1548    }
1549
1550    #[test]
1551    fn cursor_only_newlines() {
1552        cursor_equals_eager(b"\n\n\n", 1, b'\n');
1553    }
1554
1555    #[test]
1556    fn cursor_record_larger_than_chunk() {
1557        let data = b"short\nverylonglinewithnodelimiteratall\nshort\n";
1558        cursor_equals_eager(data, 6, b'\n');
1559    }
1560
1561    #[test]
1562    fn cursor_deterministic_random_corpus() {
1563        let cases: &[(&[u8], usize, u8)] = &[
1564            (b"hello\nworld\n", 4, b'\n'),
1565            (b"a,b,c,d", 2, b','),
1566            (b"one\ttwo\tthree", 4, b'\t'),
1567            (b"a|b|c|d|e|f", 3, b'|'),
1568            (b"x\x00y\x00z", 2, b'\x00'),
1569            (b"single", 1024, b'\n'),
1570            (b"\n\n\n\n\n", 1, b'\n'),
1571            (b"\n", 1, b'\n'),
1572            (b"a", 1024, b'\n'),
1573            (b"line1\nline2\nline3\nline4\nline5\n", 10, b'\n'),
1574        ];
1575        for &(data, chunk_size, delim) in cases {
1576            cursor_equals_eager(data, chunk_size, delim);
1577        }
1578    }
1579
1580    #[test]
1581    fn cursor_large_corpus_equivalence() {
1582        let mut data = Vec::new();
1583        for i in 0..1000u32 {
1584            data.extend_from_slice(format!("line_content_{i:04}\n").as_bytes());
1585        }
1586        cursor_equals_eager(&data, 64, b'\n');
1587    }
1588
1589    // ── Cursor contract tests — size_hint + is_empty ────────────────
1590
1591    #[test]
1592    fn cursor_size_hint_before_iteration() {
1593        let data = b"aaa\nbbb\nccc\nddd\neee\n";
1594        let cur = ChunkCursor::new(data, 6, b'\n');
1595        let (lo, hi) = cur.size_hint();
1596        assert_eq!(lo, 1);
1597        assert_eq!(hi, Some(data.len()));
1598    }
1599
1600    #[test]
1601    fn cursor_size_hint_after_one_next() {
1602        let data = b"aaa\nbbb\nccc\nddd\neee\n";
1603        let mut cur = ChunkCursor::new(data, 6, b'\n');
1604        let _ = cur.next();
1605        let (lo, hi) = cur.size_hint();
1606        assert_eq!(lo, 1);
1607        assert!(hi.unwrap() < data.len());
1608    }
1609
1610    #[test]
1611    fn cursor_size_hint_after_exhaustion() {
1612        let data = b"hello\n";
1613        let mut cur = ChunkCursor::new(data, 1024, b'\n');
1614        let _ = cur.next();
1615        let (lo, hi) = cur.size_hint();
1616        assert_eq!(lo, 0);
1617        assert_eq!(hi, Some(0));
1618    }
1619
1620    #[test]
1621    fn cursor_size_hint_empty_input() {
1622        let cur = ChunkCursor::new(b"", 1024, b'\n');
1623        let (lo, hi) = cur.size_hint();
1624        assert_eq!(lo, 0);
1625        assert_eq!(hi, Some(0));
1626    }
1627
1628    #[test]
1629    fn cursor_is_empty_before_iteration() {
1630        let data = b"hello\nworld";
1631        let cur = ChunkCursor::new(data, 100, b'\n');
1632        assert!(!cur.is_empty());
1633    }
1634
1635    #[test]
1636    fn cursor_is_empty_after_exhaustion() {
1637        let data = b"hello\n";
1638        let mut cur = ChunkCursor::new(data, 1024, b'\n');
1639        let _ = cur.next(); // yields the only chunk
1640        assert!(cur.is_empty());
1641    }
1642
1643    #[test]
1644    fn cursor_is_empty_empty_input() {
1645        let cur = ChunkCursor::new(b"", 1024, b'\n');
1646        assert!(cur.is_empty());
1647    }
1648
1649    #[test]
1650    fn cursor_size_hint_no_delimiter() {
1651        let data = b"no_newlines_at_all";
1652        let cur = ChunkCursor::new(data, 5, b'\n');
1653        let (lo, hi) = cur.size_hint();
1654        assert_eq!(lo, 1, "at least one chunk even with no delimiter");
1655        assert_eq!(hi, Some(data.len()));
1656    }
1657
1658    #[test]
1659    fn cursor_size_hint_chunk_size_zero() {
1660        let data = b"abcd\n";
1661        let cur = ChunkCursor::new(data, 0, b'\n');
1662        let (lo, _) = cur.size_hint();
1663        assert_eq!(lo, 1);
1664    }
1665
1666    #[test]
1667    fn cursor_size_hint_chunk_size_one() {
1668        let data = b"a\nb\nc\n";
1669        let cur = ChunkCursor::new(data, 1, b'\n');
1670        let (lo, _) = cur.size_hint();
1671        assert_eq!(lo, 1);
1672    }
1673
1674    #[test]
1675    fn cursor_size_hint_chunk_size_larger_than_data() {
1676        let data = b"tiny\n";
1677        let cur = ChunkCursor::new(data, 1_000_000, b'\n');
1678        let (lo, hi) = cur.size_hint();
1679        assert_eq!(lo, 1);
1680        assert_eq!(hi, Some(data.len()));
1681    }
1682
1683    #[test]
1684    fn property_size_hint_lower_bound_accurate() {
1685        let cases: &[(&[u8], usize, u8)] = &[
1686            (b"hello\nworld\n", 4, b'\n'),
1687            (b"a,b,c,d", 2, b','),
1688            (b"one\ttwo\tthree", 4, b'\t'),
1689            (b"a|b|c|d|e|f", 3, b'|'),
1690            (b"x\x00y\x00z", 2, b'\x00'),
1691            (b"single", 1024, b'\n'),
1692            (b"\n\n\n\n\n", 1, b'\n'),
1693            (b"", 1024, b'\n'),
1694            (b"\n", 1, b'\n'),
1695            (b"a", 1024, b'\n'),
1696            (b"a\nb\nc\n", 1, b'\n'),
1697            (b"aaaa\n", 5, b'\n'),
1698        ];
1699        for &(data, cs, delim) in cases {
1700            let mut cur = ChunkCursor::new(data, cs, delim);
1701            let mut total_yielded = 0usize;
1702            loop {
1703                let (lo, hi) = cur.size_hint();
1704                let remaining = cur.len() - cur.position();
1705                if remaining == 0 {
1706                    assert_eq!(lo, 0);
1707                    assert_eq!(hi, Some(0));
1708                    break;
1709                }
1710                assert!(
1711                    lo <= remaining,
1712                    "lower bound {lo} exceeds remaining {remaining}: data={data:?} cs={cs}"
1713                );
1714                if let Some(next) = cur.next() {
1715                    total_yielded += next.len();
1716                }
1717            }
1718            assert_eq!(total_yielded, data.len());
1719        }
1720    }
1721
1722    #[test]
1723    fn property_size_hint_upper_bound_accurate() {
1724        let cases: &[(&[u8], usize, u8)] = &[
1725            (b"hello\nworld\n", 4, b'\n'),
1726            (b"a,b,c,d", 2, b','),
1727            (b"a|b|c|d|e|f", 3, b'|'),
1728            (b"\n\n\n", 1, b'\n'),
1729        ];
1730        for &(data, cs, delim) in cases {
1731            let mut cur = ChunkCursor::new(data, cs, delim);
1732            loop {
1733                let (_, hi) = cur.size_hint();
1734                let remaining = cur.len() - cur.position();
1735                if remaining == 0 {
1736                    assert_eq!(hi, Some(0));
1737                    break;
1738                }
1739                assert!(
1740                    hi.unwrap() >= remaining,
1741                    "upper bound {} < remaining {}: data={data:?} cs={cs}",
1742                    hi.unwrap(),
1743                    remaining
1744                );
1745                let _ = cur.next();
1746            }
1747        }
1748    }
1749
1750    // ── PatternChunkCursor contract tests ────────────────────────────
1751
1752    #[test]
1753    fn pattern_cursor_size_hint_before_iteration() {
1754        let data = b"a\r\nb\r\nc\r\n";
1755        let cur = PatternChunkCursor::new(data, 4, b"\r\n");
1756        let (lo, hi) = cur.size_hint();
1757        assert_eq!(lo, 1);
1758        assert_eq!(hi, Some(data.len()));
1759    }
1760
1761    #[test]
1762    fn pattern_cursor_size_hint_after_one_next() {
1763        let data = b"a\r\nb\r\nc\r\n";
1764        let mut cur = PatternChunkCursor::new(data, 4, b"\r\n");
1765        let _ = cur.next();
1766        let (lo, hi) = cur.size_hint();
1767        assert_eq!(lo, 1);
1768        assert!(hi.unwrap() < data.len());
1769    }
1770
1771    #[test]
1772    fn pattern_cursor_size_hint_after_exhaustion() {
1773        let data = b"a\r\n";
1774        let mut cur = PatternChunkCursor::new(data, 1024, b"\r\n");
1775        let _ = cur.next();
1776        let (lo, hi) = cur.size_hint();
1777        assert_eq!(lo, 0);
1778        assert_eq!(hi, Some(0));
1779    }
1780
1781    #[test]
1782    fn pattern_cursor_size_hint_empty_input() {
1783        let cur = PatternChunkCursor::new(b"", 1024, b"\r\n");
1784        let (lo, hi) = cur.size_hint();
1785        assert_eq!(lo, 0);
1786        assert_eq!(hi, Some(0));
1787    }
1788
1789    #[test]
1790    fn pattern_cursor_is_empty_before_iteration() {
1791        let data = b"hello\r\nworld";
1792        let cur = PatternChunkCursor::new(data, 100, b"\r\n");
1793        assert!(!cur.is_empty());
1794    }
1795
1796    #[test]
1797    fn pattern_cursor_is_empty_after_exhaustion() {
1798        let data = b"hello\r\n";
1799        let mut cur = PatternChunkCursor::new(data, 1024, b"\r\n");
1800        let _ = cur.next();
1801        assert!(cur.is_empty());
1802    }
1803
1804    #[test]
1805    fn pattern_cursor_is_empty_empty_input() {
1806        let cur = PatternChunkCursor::new(b"", 1024, b"\r\n");
1807        assert!(cur.is_empty());
1808    }
1809
1810    #[test]
1811    fn pattern_cursor_size_hint_no_delimiter() {
1812        let data = b"no_crlf_here";
1813        let cur = PatternChunkCursor::new(data, 5, b"\r\n");
1814        let (lo, _) = cur.size_hint();
1815        assert_eq!(lo, 1);
1816    }
1817
1818    #[test]
1819    fn pattern_property_size_hint_lower_bound_accurate() {
1820        let cases: &[(&[u8], usize, &[u8])] = &[
1821            (b"a\r\nb\r\nc\r\n", 4, b"\r\n"),
1822            (b"ab||cd||ef", 4, b"||"),
1823            (b"single", 1024, b"\r\n"),
1824            (b"AB\xff\x00CD\xff\x00EF", 4, b"\xff\x00"),
1825            (b"\r\n\r\n\r\n", 1, b"\r\n"),
1826        ];
1827        for &(data, cs, delim) in cases {
1828            let mut cur = PatternChunkCursor::new(data, cs, delim);
1829            let mut total_yielded = 0usize;
1830            loop {
1831                let (lo, hi) = cur.size_hint();
1832                let remaining = cur.len() - cur.position();
1833                if remaining == 0 {
1834                    assert_eq!(lo, 0);
1835                    assert_eq!(hi, Some(0));
1836                    break;
1837                }
1838                assert!(
1839                    lo <= remaining,
1840                    "lower bound {lo} exceeds remaining {remaining}"
1841                );
1842                if let Some(next) = cur.next() {
1843                    total_yielded += next.len();
1844                }
1845            }
1846            assert_eq!(total_yielded, data.len());
1847        }
1848    }
1849
1850    // ── Fixed-size chunking tests ────────────────────────────────────────
1851
1852    #[test]
1853    #[ignore = "performance experiment — run with --ignored --nocapture"]
1854    fn bench_cursor_vs_eager() {
1855        use std::hint::black_box;
1856        use std::time::Instant;
1857
1858        const SAMPLES: usize = 7;
1859
1860        fn gen_log_data(target_size: usize) -> Vec<u8> {
1861            let mut data = Vec::with_capacity(target_size);
1862            let mut n = 0u64;
1863            while data.len() < target_size {
1864                let line = format!(
1865                    "[2026-08-08T12:00:00Z] INFO request_id={} status=200 latency_ms={}\n",
1866                    n,
1867                    n % 100
1868                );
1869                data.extend_from_slice(line.as_bytes());
1870                n += 1;
1871            }
1872            data.truncate(target_size);
1873            data
1874        }
1875
1876        fn elapsed_per_iter(iters: u64, f: impl Fn()) -> f64 {
1877            let start = Instant::now();
1878            for _ in 0..iters {
1879                f();
1880            }
1881            start.elapsed().as_nanos() as f64 / iters as f64
1882        }
1883
1884        fn samples_median(mut ns: Vec<f64>) -> f64 {
1885            ns.sort_by(|a, b| a.partial_cmp(b).unwrap());
1886            ns[ns.len() / 2]
1887        }
1888
1889        fn samples_p10(mut ns: Vec<f64>) -> f64 {
1890            ns.sort_by(|a, b| a.partial_cmp(b).unwrap());
1891            let idx = ((ns.len() - 1) as f64 * 0.10) as usize;
1892            ns[idx]
1893        }
1894
1895        fn samples_p90(mut ns: Vec<f64>) -> f64 {
1896            ns.sort_by(|a, b| a.partial_cmp(b).unwrap());
1897            let idx = ((ns.len() - 1) as f64 * 0.90) as usize;
1898            ns[idx]
1899        }
1900
1901        fn bench_one(
1902            data: &[u8],
1903            chunk_size: usize,
1904            delim: u8,
1905            label: &str,
1906            iters: u64,
1907            eager_chunks: usize,
1908            lazy_chunks: usize,
1909        ) {
1910            let total_bytes = data.len();
1911
1912            let mut tfc_eager_ns: Vec<f64> = Vec::with_capacity(SAMPLES);
1913            let mut tfc_lazy_ns: Vec<f64> = Vec::with_capacity(SAMPLES);
1914            let mut full_eager_ns: Vec<f64> = Vec::with_capacity(SAMPLES);
1915            let mut full_lazy_ns: Vec<f64> = Vec::with_capacity(SAMPLES);
1916
1917            for _ in 0..SAMPLES {
1918                tfc_eager_ns.push(elapsed_per_iter(iters, || {
1919                    let d = black_box(data);
1920                    let cs = black_box(chunk_size);
1921                    let dl = black_box(delim);
1922                    let chunks = find_chunk_boundaries(d, cs, dl);
1923                    black_box(chunks.first().copied());
1924                }));
1925
1926                tfc_lazy_ns.push(elapsed_per_iter(iters, || {
1927                    let d = black_box(data);
1928                    let cs = black_box(chunk_size);
1929                    let dl = black_box(delim);
1930                    let mut cursor = ChunkCursor::new(d, cs, dl);
1931                    black_box(cursor.next());
1932                }));
1933
1934                full_eager_ns.push(elapsed_per_iter(iters, || {
1935                    let d = black_box(data);
1936                    let cs = black_box(chunk_size);
1937                    let dl = black_box(delim);
1938                    let chunks = find_chunk_boundaries(d, cs, dl);
1939                    let mut total = 0usize;
1940                    for &(s, e) in &chunks {
1941                        total = total.wrapping_add(e - s);
1942                    }
1943                    black_box(total);
1944                    black_box(&chunks);
1945                }));
1946
1947                full_lazy_ns.push(elapsed_per_iter(iters, || {
1948                    let d = black_box(data);
1949                    let cs = black_box(chunk_size);
1950                    let dl = black_box(delim);
1951                    let mut total = 0usize;
1952                    let cursor = ChunkCursor::new(d, cs, dl);
1953                    for chunk in cursor {
1954                        total = total.wrapping_add(chunk.len());
1955                    }
1956                    black_box(total);
1957                }));
1958            }
1959
1960            let tfc_e_p50 = samples_median(tfc_eager_ns.clone());
1961            let tfc_l_p50 = samples_median(tfc_lazy_ns.clone());
1962            let full_e_p50 = samples_median(full_eager_ns.clone());
1963            let full_l_p50 = samples_median(full_lazy_ns.clone());
1964
1965            let tfc_e_p10 = samples_p10(tfc_eager_ns.clone());
1966            let tfc_l_p10 = samples_p10(tfc_lazy_ns.clone());
1967            let full_e_p10 = samples_p10(full_eager_ns.clone());
1968            let full_l_p10 = samples_p10(full_lazy_ns.clone());
1969
1970            let tfc_e_p90 = samples_p90(tfc_eager_ns.clone());
1971            let tfc_l_p90 = samples_p90(tfc_lazy_ns.clone());
1972            let full_e_p90 = samples_p90(full_eager_ns.clone());
1973            let full_l_p90 = samples_p90(full_lazy_ns.clone());
1974
1975            println!();
1976            println!("  === {label} ===");
1977            println!("  file={total_bytes}B chunk={chunk_size}B delim=0x{delim:02x}",);
1978            println!(
1979                "  build={} samples={SAMPLES} iters/sample={iters}",
1980                if cfg!(debug_assertions) {
1981                    "debug"
1982                } else {
1983                    "release"
1984                },
1985            );
1986            println!("  eager_chunks={eager_chunks}  lazy_chunks={lazy_chunks}");
1987            println!(
1988                "  TFC  eager  p50={:>10.1}ns  p10={:>10.1}  p90={:>10.1}",
1989                tfc_e_p50, tfc_e_p10, tfc_e_p90,
1990            );
1991            println!(
1992                "  TFC  lazy   p50={:>10.1}ns  p10={:>10.1}  p90={:>10.1}  ratio={:.2}x",
1993                tfc_l_p50,
1994                tfc_l_p10,
1995                tfc_l_p90,
1996                tfc_e_p50 / tfc_l_p50,
1997            );
1998            println!(
1999                "  Full eager  p50={:>10.1}ns  p10={:>10.1}  p90={:>10.1}",
2000                full_e_p50, full_e_p10, full_e_p90,
2001            );
2002            println!(
2003                "  Full lazy   p50={:>10.1}ns  p10={:>10.1}  p90={:>10.1}  ratio={:.2}x",
2004                full_l_p50,
2005                full_l_p10,
2006                full_l_p90,
2007                full_e_p50 / full_l_p50,
2008            );
2009        }
2010
2011        println!("=== Time-to-First-Chunk + Full Traversal ===");
2012        println!(
2013            "  CPU: {} cores",
2014            std::thread::available_parallelism()
2015                .map(|n| n.get())
2016                .unwrap_or(1)
2017        );
2018        println!("  OS:  {}", std::env::consts::OS);
2019
2020        let jsonl = gen_log_data(100_000);
2021        let logs = gen_log_data(1_000_000);
2022        let sparse = gen_log_data(10_000_000);
2023
2024        let jsonl_ec = find_chunk_boundaries(&jsonl, 64 * 1024, b'\n').len();
2025        let jsonl_lc = ChunkCursor::new(&jsonl, 64 * 1024, b'\n').count();
2026        bench_one(
2027            &jsonl,
2028            64 * 1024,
2029            b'\n',
2030            "JSONL-like ~100B rec 100KB 64KiB",
2031            200,
2032            jsonl_ec,
2033            jsonl_lc,
2034        );
2035
2036        let logs_ec = find_chunk_boundaries(&logs, 64 * 1024, b'\n').len();
2037        let logs_lc = ChunkCursor::new(&logs, 64 * 1024, b'\n').count();
2038        bench_one(
2039            &logs,
2040            64 * 1024,
2041            b'\n',
2042            "Log-like ~100B rec 1MB 64KiB",
2043            50,
2044            logs_ec,
2045            logs_lc,
2046        );
2047
2048        let sparse_ec = find_chunk_boundaries(&sparse, 64 * 1024, b'\n').len();
2049        let sparse_lc = ChunkCursor::new(&sparse, 64 * 1024, b'\n').count();
2050        bench_one(
2051            &sparse,
2052            64 * 1024,
2053            b'\n',
2054            "Sparse ~100B rec 10MB 64KiB",
2055            10,
2056            sparse_ec,
2057            sparse_lc,
2058        );
2059
2060        let jsonl_ec_1m = find_chunk_boundaries(&jsonl, 1024 * 1024, b'\n').len();
2061        let jsonl_lc_1m = ChunkCursor::new(&jsonl, 1024 * 1024, b'\n').count();
2062        bench_one(
2063            &jsonl,
2064            1024 * 1024,
2065            b'\n',
2066            "JSONL-like ~100B rec 100KB 1MiB",
2067            200,
2068            jsonl_ec_1m,
2069            jsonl_lc_1m,
2070        );
2071
2072        let logs_ec_1m = find_chunk_boundaries(&logs, 1024 * 1024, b'\n').len();
2073        let logs_lc_1m = ChunkCursor::new(&logs, 1024 * 1024, b'\n').count();
2074        bench_one(
2075            &logs,
2076            1024 * 1024,
2077            b'\n',
2078            "Log-like ~100B rec 1MB 1MiB",
2079            50,
2080            logs_ec_1m,
2081            logs_lc_1m,
2082        );
2083    }
2084
2085    // ── Fixed-size chunking tests ────────────────────────────────────────
2086
2087    #[test]
2088    fn test_fixed_chunk_count_empty() {
2089        assert_eq!(fixed_chunk_count(0, 1024), 0);
2090    }
2091
2092    #[test]
2093    fn test_fixed_chunk_count_exact_multiple() {
2094        assert_eq!(fixed_chunk_count(1024, 256), 4);
2095    }
2096
2097    #[test]
2098    fn test_fixed_chunk_count_with_remainder() {
2099        assert_eq!(fixed_chunk_count(1000, 256), 4);
2100    }
2101
2102    #[test]
2103    fn test_fixed_chunk_count_single() {
2104        assert_eq!(fixed_chunk_count(10, 1024), 1);
2105    }
2106
2107    #[test]
2108    fn test_fixed_chunk_count_zero_clamps() {
2109        assert_eq!(fixed_chunk_count(5, 0), 5);
2110    }
2111
2112    #[test]
2113    fn test_fixed_chunk_bounds_empty() {
2114        assert_eq!(fixed_chunk_bounds(0, 256, 0), None);
2115    }
2116
2117    #[test]
2118    fn test_fixed_chunk_bounds_exact() {
2119        assert_eq!(fixed_chunk_bounds(1024, 256, 0), Some((0, 256)));
2120        assert_eq!(fixed_chunk_bounds(1024, 256, 1), Some((256, 512)));
2121        assert_eq!(fixed_chunk_bounds(1024, 256, 2), Some((512, 768)));
2122        assert_eq!(fixed_chunk_bounds(1024, 256, 3), Some((768, 1024)));
2123        assert_eq!(fixed_chunk_bounds(1024, 256, 4), None);
2124    }
2125
2126    #[test]
2127    fn test_fixed_chunk_bounds_remainder() {
2128        assert_eq!(fixed_chunk_bounds(1000, 256, 3), Some((768, 1000)));
2129    }
2130
2131    #[test]
2132    fn test_fixed_chunk_bounds_size_larger_than_file() {
2133        assert_eq!(fixed_chunk_bounds(10, 1024, 0), Some((0, 10)));
2134        assert_eq!(fixed_chunk_bounds(10, 1024, 1), None);
2135    }
2136
2137    #[test]
2138    fn test_fixed_chunk_bounds_oob() {
2139        assert_eq!(fixed_chunk_bounds(1024, 256, 4), None);
2140        assert_eq!(fixed_chunk_bounds(1024, 256, 100), None);
2141    }
2142
2143    #[test]
2144    fn test_fixed_chunk_bounds_zero_clamps() {
2145        assert_eq!(fixed_chunk_bounds(5, 0, 0), Some((0, 1)));
2146        assert_eq!(fixed_chunk_bounds(5, 0, 1), Some((1, 2)));
2147        assert_eq!(fixed_chunk_bounds(5, 0, 4), Some((4, 5)));
2148        assert_eq!(fixed_chunk_bounds(5, 0, 5), None);
2149    }
2150
2151    #[test]
2152    fn test_fixed_chunk_bounds_single() {
2153        assert_eq!(fixed_chunk_bounds(1, 1024, 0), Some((0, 1)));
2154    }
2155
2156    #[test]
2157    fn test_fixed_property_concat_equals_len() {
2158        let cases: &[(usize, usize)] = &[
2159            (1024, 256),
2160            (1000, 256),
2161            (1, 1024),
2162            (0, 1024),
2163            (5, 1),
2164            (1024, 1024),
2165            (1025, 1024),
2166        ];
2167        for &(len, cs) in cases {
2168            let count = fixed_chunk_count(len, cs);
2169            let mut total = 0usize;
2170            for i in 0..count {
2171                let (s, e) = fixed_chunk_bounds(len, cs, i).unwrap();
2172                total += e - s;
2173                assert!(s <= e);
2174            }
2175            assert_eq!(total, len, "len={len} cs={cs}");
2176        }
2177    }
2178
2179    #[test]
2180    fn test_fixed_property_no_gaps() {
2181        let cases: &[(usize, usize)] = &[(1024, 256), (1000, 256), (5, 1)];
2182        for &(len, cs) in cases {
2183            let count = fixed_chunk_count(len, cs);
2184            let mut prev_end = 0usize;
2185            for i in 0..count {
2186                let (s, e) = fixed_chunk_bounds(len, cs, i).unwrap();
2187                assert_eq!(s, prev_end, "gap at i={i}");
2188                prev_end = e;
2189            }
2190            assert_eq!(prev_end, len);
2191        }
2192    }
2193
2194    #[test]
2195    fn test_fixed_property_non_final_full() {
2196        let cases: &[(usize, usize)] = &[(1024, 256), (1000, 256), (5, 1)];
2197        for &(len, cs) in cases {
2198            let count = fixed_chunk_count(len, cs);
2199            let eff = cs.max(1);
2200            for i in 0..count.saturating_sub(1) {
2201                let (s, e) = fixed_chunk_bounds(len, cs, i).unwrap();
2202                assert_eq!(e - s, eff, "non-final chunk {i} not full");
2203            }
2204        }
2205    }
2206
2207    #[test]
2208    fn test_fixed_property_final_not_larger_than_chunk_size() {
2209        let cases: &[(usize, usize)] = &[(1024, 256), (1000, 256), (5, 1)];
2210        for &(len, cs) in cases {
2211            let count = fixed_chunk_count(len, cs);
2212            if count > 0 {
2213                let (s, e) = fixed_chunk_bounds(len, cs, count - 1).unwrap();
2214                assert!(e - s <= cs.max(1));
2215            }
2216        }
2217    }
2218
2219    #[test]
2220    fn test_fixed_property_deterministic() {
2221        for _ in 0..10 {
2222            assert_eq!(fixed_chunk_count(1000, 256), 4);
2223            assert_eq!(fixed_chunk_bounds(1000, 256, 1), Some((256, 512)));
2224        }
2225    }
2226
2227    #[test]
2228    fn test_fixed_overflow_safety() {
2229        // chunk_size near usize::MAX — should produce 1 chunk covering the file
2230        let len: usize = 1024;
2231        let huge: usize = usize::MAX;
2232        assert_eq!(fixed_chunk_count(len, huge), 1);
2233        assert_eq!(fixed_chunk_bounds(len, huge, 0), Some((0, len)));
2234
2235        // chunk_size = 1 on reasonable file
2236        assert_eq!(fixed_chunk_count(len, 1), len);
2237        assert_eq!(fixed_chunk_bounds(len, 1, 0), Some((0, 1)));
2238        assert_eq!(fixed_chunk_bounds(len, 1, len - 1), Some((len - 1, len)));
2239
2240        // zero file, huge chunk
2241        assert_eq!(fixed_chunk_count(0, usize::MAX), 0);
2242    }
2243
2244    // ── SWAR byte-search correctness & performance (internal) ────────────
2245
2246    mod swar_bench {
2247        use super::find_byte_swar;
2248        use std::hint::black_box;
2249        use std::time::Instant;
2250
2251        #[inline(always)]
2252        fn find_byte_scalar(haystack: &[u8], delimiter: u8) -> Option<usize> {
2253            haystack.iter().position(|&b| b == delimiter)
2254        }
2255
2256        // ── Correctness oracle ──────────────────────────────────────
2257
2258        const DELIMITERS: &[u8] = &[0x00, 0x01, b'\n', b',', b'|', 0x7f, 0x80, 0xfe, 0xff];
2259        const LENGTHS: &[usize] = &[
2260            0, 1, 2, 3, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64, 65, 127, 128, 129, 200, 256,
2261        ];
2262
2263        fn assert_swar_eq_scalar(data: &[u8], delim: u8, label: &str) {
2264            let s = find_byte_scalar(data, delim);
2265            let w = find_byte_swar(data, delim);
2266            assert_eq!(
2267                s,
2268                w,
2269                "SWAR != scalar: {label}  data[0]={:02x} len={} delim={:02x}",
2270                data.first().copied().unwrap_or(0),
2271                data.len(),
2272                delim
2273            );
2274        }
2275
2276        fn deterministic_bytes(seed: u64, len: usize) -> Vec<u8> {
2277            let mut state = seed;
2278            (0..len)
2279                .map(|_| {
2280                    state = state
2281                        .wrapping_mul(6364136223846793005)
2282                        .wrapping_add(1442695040888963407);
2283                    (state >> 32) as u8
2284                })
2285                .collect()
2286        }
2287
2288        #[test]
2289        fn correctness_empty() {
2290            for &d in DELIMITERS {
2291                assert_swar_eq_scalar(&[], d, "empty");
2292            }
2293        }
2294
2295        #[test]
2296        fn correctness_all_lengths_no_match() {
2297            for &len in LENGTHS {
2298                let delim = b'\n';
2299                let data = vec![b'x'; len];
2300                assert_swar_eq_scalar(&data, delim, &format!("len={len} nomatch"));
2301            }
2302        }
2303
2304        #[test]
2305        fn correctness_all_lengths_all_match() {
2306            for &len in LENGTHS {
2307                if len == 0 {
2308                    continue;
2309                }
2310                let delim = b'\n';
2311                let data = vec![delim; len];
2312                assert_swar_eq_scalar(&data, delim, &format!("len={len} allmatch"));
2313            }
2314        }
2315
2316        #[test]
2317        fn correctness_match_at_every_position() {
2318            for &len in &[1, 8, 16, 32, 64, 128, 200] {
2319                for pos in 0..len {
2320                    let delim = b'\n';
2321                    let mut data = vec![b'x'; len];
2322                    data[pos] = delim;
2323                    assert_swar_eq_scalar(&data, delim, &format!("len={len} pos={pos}"));
2324                }
2325            }
2326        }
2327
2328        #[test]
2329        fn correctness_all_delimiters() {
2330            let len = 256usize;
2331            for &delim in DELIMITERS {
2332                let other: u8 = if delim == 0x00 { 0x01 } else { 0x00 };
2333                let data = vec![other; len];
2334                assert_swar_eq_scalar(&data, delim, &format!("delim={delim:02x} nomatch"));
2335                let mut data = vec![other; len];
2336                data[len / 2] = delim;
2337                assert_swar_eq_scalar(&data, delim, &format!("delim={delim:02x} middle"));
2338            }
2339        }
2340
2341        #[test]
2342        fn correctness_unaligned_starts() {
2343            let base = deterministic_bytes(42, 256);
2344            for start in 0..16 {
2345                for &delim in &[b'\n', b',', 0x00, 0xff] {
2346                    let slice = &base[start..];
2347                    assert_swar_eq_scalar(
2348                        slice,
2349                        delim,
2350                        &format!("unaligned start={start} delim={delim:02x}"),
2351                    );
2352                }
2353            }
2354        }
2355
2356        #[test]
2357        fn correctness_match_at_boundaries() {
2358            let delim = b'\n';
2359            for &len in &[8, 9, 15, 16, 17, 24, 25, 31, 32, 33] {
2360                let mut data = vec![b'x'; len];
2361                for pos in [0, 7, 8, 15, 16, 23, 24, 31, 32].iter().copied() {
2362                    if pos < len {
2363                        data[pos] = delim;
2364                        assert_swar_eq_scalar(
2365                            &data,
2366                            delim,
2367                            &format!("len={len} word-boundary pos={pos}"),
2368                        );
2369                        data[pos] = b'x';
2370                    }
2371                }
2372                data[len - 1] = delim;
2373                assert_swar_eq_scalar(&data, delim, &format!("len={len} last-byte"));
2374            }
2375        }
2376
2377        #[test]
2378        fn correctness_deterministic_random() {
2379            for seed in 0..20u64 {
2380                let data = deterministic_bytes(seed, 1024);
2381                for delim in [b'\n', b',', b'|', 0x00, 0xff] {
2382                    assert_swar_eq_scalar(
2383                        &data,
2384                        delim,
2385                        &format!("random seed={seed} delim={delim:02x}"),
2386                    );
2387                }
2388            }
2389        }
2390
2391        #[test]
2392        fn correctness_many_matches() {
2393            let delim = b'\n';
2394            let mut data = Vec::with_capacity(10000);
2395            for i in 0..1000 {
2396                data.extend_from_slice(b"line_content_");
2397                data.extend_from_slice(&(i as u32).to_le_bytes());
2398                data.push(delim);
2399            }
2400            for &delim_test in &[b'\n', b'l', b'_', 0x00, 0xff] {
2401                assert_swar_eq_scalar(
2402                    &data,
2403                    delim_test,
2404                    &format!("many_matches delim={delim_test:02x}"),
2405                );
2406            }
2407        }
2408
2409        // ── Byte-search microbenchmark ───────────────────────────────
2410
2411        fn bench_find_byte<F>(haystack: &[u8], delim: u8, f: F, iters: u64) -> f64
2412        where
2413            F: Fn(&[u8], u8) -> Option<usize>,
2414        {
2415            let start = Instant::now();
2416            for _ in 0..iters {
2417                let result = f(black_box(haystack), delim);
2418                black_box(result);
2419            }
2420            start.elapsed().as_nanos() as f64 / iters as f64
2421        }
2422
2423        fn bench_find_byte_auto<F>(haystack: &[u8], delim: u8, f: F, name: &str) -> (f64, usize)
2424        where
2425            F: Fn(&[u8], u8) -> Option<usize>,
2426        {
2427            let warmup_iters = 10;
2428            for _ in 0..warmup_iters {
2429                black_box(f(black_box(haystack), delim));
2430            }
2431
2432            let mut iters: u64 = 100;
2433            let max_iters: u64 = 100_000;
2434            let mut samples = Vec::with_capacity(10);
2435
2436            while iters <= max_iters {
2437                let avg_ns = bench_find_byte(haystack, delim, &f, iters);
2438                if avg_ns * iters as f64 > 500_000_000.0 {
2439                    samples.push(avg_ns);
2440                    if samples.len() >= 5 {
2441                        break;
2442                    }
2443                }
2444                iters = (iters * 2).min(max_iters);
2445                if iters == max_iters && samples.is_empty() {
2446                    samples.push(avg_ns);
2447                    break;
2448                }
2449            }
2450
2451            if samples.is_empty() {
2452                return (0.0, 0);
2453            }
2454
2455            let n = samples.len();
2456            let mut sorted = samples.clone();
2457            sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
2458            let median_ns = sorted[n / 2];
2459
2460            let result = black_box(f(black_box(haystack), delim));
2461            let _ = black_box(result);
2462
2463            println!(
2464                "  {name:<20} {median_ns:>10.1} ns/call  (n={n})",
2465                name = name,
2466                n = n
2467            );
2468            (median_ns, n)
2469        }
2470
2471        #[test]
2472        #[ignore = "performance experiment — run with --ignored --nocapture"]
2473        fn bench_byte_search_primitive() {
2474            println!();
2475            println!("=== Byte-Search Microbenchmark (scalar vs SWAR) ===");
2476            println!(
2477                "  Build: {}",
2478                if cfg!(debug_assertions) {
2479                    "debug"
2480                } else {
2481                    "release"
2482                }
2483            );
2484
2485            let delim = b'\n';
2486
2487            fn gen_data(len: usize, pattern: &str) -> Vec<u8> {
2488                match pattern {
2489                    "no_match" => vec![b'x'; len],
2490                    "match_mid" => {
2491                        let mut d = vec![b'x'; len];
2492                        d[len / 2] = b'\n';
2493                        d
2494                    }
2495                    "match_end" => {
2496                        let mut d = vec![b'x'; len];
2497                        d[len - 1] = b'\n';
2498                        d
2499                    }
2500                    "match_first" => {
2501                        let mut d = vec![b'x'; len];
2502                        d[0] = b'\n';
2503                        d
2504                    }
2505                    _ => vec![b'x'; len],
2506                }
2507            }
2508
2509            let sizes: &[(usize, &str)] = &[
2510                (32, "Tiny"),
2511                (64, "1 cache line"),
2512                (128, "2 cache lines"),
2513                (256, "4 cache lines"),
2514                (4096, "4 KiB page"),
2515                (65536, "64 KiB"),
2516                (1048576, "1 MiB"),
2517            ];
2518
2519            println!("  Case                       Scalar (ns)   SWAR (ns)   Ratio");
2520
2521            for &(size, label) in sizes {
2522                for pattern in ["no_match", "match_mid"] {
2523                    let data = gen_data(size, pattern);
2524                    let case = format!("{label} {size}B {pattern}");
2525                    let (s, _) = bench_find_byte_auto(
2526                        &data,
2527                        delim,
2528                        find_byte_scalar,
2529                        &format!("scalar {case}"),
2530                    );
2531                    let (w, _) = bench_find_byte_auto(
2532                        &data,
2533                        delim,
2534                        find_byte_swar,
2535                        &format!("SWAR   {case}"),
2536                    );
2537                    let ratio = if s > 0.0 { w / s } else { f64::NAN };
2538                    println!(
2539                        "  {case:<25} {s:>14.1} {w:>14.1} {ratio:>9.2}x",
2540                        case = case,
2541                        s = s,
2542                        w = w,
2543                        ratio = ratio
2544                    );
2545                }
2546            }
2547        }
2548    }
2549
2550    // ── Partition planning tests ──────────────────────────────────────
2551
2552    #[test]
2553    fn partition_empty_data() {
2554        assert_eq!(find_partition_boundaries(b"", 4, b'\n'), vec![]);
2555    }
2556
2557    #[test]
2558    fn partition_zero_partitions() {
2559        let data = b"hello\nworld\n";
2560        assert_eq!(find_partition_boundaries(data, 0, b'\n'), vec![]);
2561    }
2562
2563    #[test]
2564    fn partition_request_far_above_file_len_is_bounded() {
2565        let data = b"a\nb\nc\n";
2566
2567        assert_eq!(
2568            find_partition_boundaries(data, usize::MAX, b'\n'),
2569            vec![(0, 2), (2, 4), (4, 6)]
2570        );
2571    }
2572
2573    #[test]
2574    fn partition_single_partition() {
2575        let data = b"hello\nworld\n";
2576        assert_eq!(
2577            find_partition_boundaries(data, 1, b'\n'),
2578            vec![(0, data.len())]
2579        );
2580    }
2581
2582    #[test]
2583    fn partition_no_delimiter() {
2584        let data = b"no_newlines_here";
2585        for &n in &[2, 4, 8, 16] {
2586            let partitions = find_partition_boundaries(data, n, b'\n');
2587            assert_eq!(partitions, vec![(0, data.len())]);
2588        }
2589    }
2590
2591    #[test]
2592    fn partition_block_count_semantics() {
2593        let data = b"a\nb\nc\nd\ne\n";
2594        // 5 records, request 2 partitions
2595        let partitions = find_partition_boundaries(data, 2, b'\n');
2596        assert!(partitions.len() >= 2);
2597        // Complete coverage
2598        assert_eq!(partitions[0].0, 0);
2599        assert_eq!(partitions.last().unwrap().1, data.len());
2600    }
2601
2602    #[test]
2603    fn partition_property_no_gaps() {
2604        let cases: &[(&[u8], usize, u8)] = &[
2605            (b"a\nb\nc\n", 2, b'\n'),
2606            (b"a\nb\nc\n", 3, b'\n'),
2607            (b"a\nb\nc\n", 4, b'\n'),
2608            (b"a,b,c,d,e,f", 3, b','),
2609            (b"a\tb\tc\t", 2, b'\t'),
2610            (b"a|b|c|d|e|f", 4, b'|'),
2611        ];
2612        for &(data, n, delim) in cases {
2613            let partitions = find_partition_boundaries(data, n, delim);
2614            if partitions.is_empty() {
2615                continue;
2616            }
2617            assert_eq!(partitions[0].0, 0, "first must start at 0");
2618            for i in 1..partitions.len() {
2619                assert_eq!(
2620                    partitions[i].0,
2621                    partitions[i - 1].1,
2622                    "gap at partition {}->{}",
2623                    i - 1,
2624                    i
2625                );
2626            }
2627            assert_eq!(
2628                partitions.last().unwrap().1,
2629                data.len(),
2630                "last must end at EOF"
2631            );
2632        }
2633    }
2634
2635    #[test]
2636    fn partition_property_concatenation_equals_input() {
2637        let cases: &[(&[u8], usize, u8)] = &[
2638            (b"hello\nworld\n", 2, b'\n'),
2639            (b"a,b,c,d", 2, b','),
2640            (b"one\ttwo\tthree", 2, b'\t'),
2641            (b"a|b|c|d|e|f", 3, b'|'),
2642            (b"x\x00y\x00z", 2, b'\x00'),
2643            (b"single", 4, b'\n'),
2644            (b"\n\n\n\n\n", 2, b'\n'),
2645            (b"\n", 2, b'\n'),
2646            (b"a", 4, b'\n'),
2647        ];
2648        for &(data, n, delim) in cases {
2649            let partitions = find_partition_boundaries(data, n, delim);
2650            let total: usize = partitions.iter().map(|(s, e)| e - s).sum();
2651            assert_eq!(
2652                total,
2653                data.len(),
2654                "concat property failed: n={n} delim={delim:02x}"
2655            );
2656        }
2657    }
2658
2659    #[test]
2660    fn partition_property_determinism() {
2661        let data = b"line1\nline2\nline3\nline4\nline5\n";
2662        let p1 = find_partition_boundaries(data, 3, b'\n');
2663        let p2 = find_partition_boundaries(data, 3, b'\n');
2664        assert_eq!(p1, p2);
2665        let p3 = find_partition_boundaries(data, 3, b'\n');
2666        assert_eq!(p1, p3);
2667    }
2668
2669    #[test]
2670    fn partition_property_boundary_after_delimiter() {
2671        let data = b"record1\nrecord2\nrecord3\n";
2672        for &n in &[2, 3, 4] {
2673            let partitions = find_partition_boundaries(data, n, b'\n');
2674            // Every non-final partition must end right after a newline
2675            for (_start, end) in partitions.iter().take(partitions.len().saturating_sub(1)) {
2676                assert_eq!(
2677                    data[end.wrapping_sub(1)],
2678                    b'\n',
2679                    "non-final partition must end after delimiter"
2680                );
2681                assert!(*end > 0, "partition must be non-empty");
2682            }
2683        }
2684    }
2685
2686    #[test]
2687    fn partition_property_no_empty_partitions() {
2688        let data = b"a\nb\nc\n";
2689        for &n in &[2, 3, 4, 8] {
2690            let partitions = find_partition_boundaries(data, n, b'\n');
2691            for &(start, end) in &partitions {
2692                assert!(end > start, "zero-length partition: n={n}");
2693            }
2694        }
2695    }
2696
2697    #[test]
2698    fn partition_property_monotonic() {
2699        let data = b"x\nxx\nxxx\nxxxx\nxxxxx\n";
2700        for &n in &[2, 3, 4, 8] {
2701            let partitions = find_partition_boundaries(data, n, b'\n');
2702            let mut last_end = 0usize;
2703            for (start, end) in &partitions {
2704                assert!(*start >= last_end);
2705                assert!(*end > *start);
2706                last_end = *end;
2707            }
2708        }
2709    }
2710
2711    // ── Record integrity — numbered records ─────────────────────────
2712
2713    #[test]
2714    fn partition_record_integrity_numbered() {
2715        let delim = b'\n';
2716        let mut data = Vec::new();
2717        let mut original_records: Vec<Vec<u8>> = Vec::new();
2718        for i in 0..100u32 {
2719            let record = format!("record-{i:06}\n").into_bytes();
2720            data.extend_from_slice(&record);
2721            original_records.push(record);
2722        }
2723
2724        // Ensure data starts at 0
2725        let file_len = data.len();
2726
2727        for &n in &[1, 2, 3, 5, 7, 8, 13, 17, 20, 37, 50] {
2728            let partitions = find_partition_boundaries(&data, n, delim);
2729
2730            // Verify complete coverage
2731            let total: usize = partitions.iter().map(|(s, e)| e - s).sum();
2732            assert_eq!(total, file_len);
2733
2734            // Collect all records as bytes from each partition
2735            let mut recovered: Vec<Vec<u8>> = Vec::new();
2736            for &(start, end) in &partitions {
2737                let chunk = &data[start..end];
2738                // Split chunk by delimiter (simple line parser)
2739                let mut pos = 0;
2740                for (j, &b) in chunk.iter().enumerate() {
2741                    if b == delim {
2742                        let rec = chunk[pos..=j].to_vec();
2743                        if !rec.is_empty() {
2744                            recovered.push(rec);
2745                        }
2746                        pos = j + 1;
2747                    }
2748                }
2749                // Handle trailing content without delimiter
2750                if pos < chunk.len() {
2751                    recovered.push(chunk[pos..].to_vec());
2752                }
2753            }
2754
2755            // Must recover exactly original records in order
2756            assert_eq!(
2757                recovered, original_records,
2758                "record integrity violated for n={n}"
2759            );
2760        }
2761    }
2762
2763    #[test]
2764    fn partition_record_integrity_giant_record() {
2765        // One giant record in the middle, records on both sides
2766        let delim = b'\n';
2767        let mut data = b"short1\n".to_vec();
2768        let giant = vec![b'x'; 5000];
2769        // Giant has NO delimiter — spans many target positions
2770        data.extend_from_slice(&giant);
2771        data.extend_from_slice(b"short2\n");
2772        let file_len = data.len();
2773
2774        for &n in &[2, 4, 8, 16, 32] {
2775            let partitions = find_partition_boundaries(&data, n, delim);
2776
2777            // Complete coverage
2778            let total: usize = partitions.iter().map(|(s, e)| e - s).sum();
2779            assert_eq!(total, file_len);
2780
2781            // The giant record should not be split
2782            // Check non-final partition boundaries
2783            for (_start, end) in partitions.iter().take(partitions.len().saturating_sub(1)) {
2784                assert_eq!(
2785                    data[end.wrapping_sub(1)],
2786                    delim,
2787                    "non-final partition must end after delimiter at n={n}"
2788                );
2789            }
2790
2791            // If n <= number of actual records, should have fewer or equal partitions
2792            // With 2 records + 1 giant no-delim block, max partitions = 3
2793            assert!(partitions.len() <= 3 + 1);
2794        }
2795    }
2796
2797    // ── Balance metrics ──────────────────────────────────────────────
2798
2799    #[test]
2800    fn partition_balance_uniform_100b() {
2801        let delim = b'\n';
2802        let record_size = 100;
2803        let record_count = 1000;
2804        let mut data = Vec::new();
2805        for i in 0..record_count {
2806            let fill = (i as u8).wrapping_add(0x41);
2807            let payload = vec![fill; record_size - 1];
2808            data.extend_from_slice(&payload);
2809            data.push(delim);
2810        }
2811        let file_len = data.len();
2812
2813        fn max_abs_deviation(partitions: &[(usize, usize)], ideal: usize) -> usize {
2814            partitions
2815                .iter()
2816                .map(|(s, e)| {
2817                    let size = e - s;
2818                    size.abs_diff(ideal)
2819                })
2820                .max()
2821                .unwrap_or(0)
2822        }
2823
2824        for &n in &[2, 4, 8, 16, 32] {
2825            let partitions = find_partition_boundaries(&data, n, delim);
2826            let actual_n = partitions.len();
2827            let ideal = file_len / actual_n;
2828            let mad = max_abs_deviation(&partitions, ideal);
2829
2830            // For uniform 100B records, deviation should be bounded by ~2 * record_size
2831            assert!(
2832                mad <= 2 * record_size + 10,
2833                "n={n}, ideal={ideal}, mad={mad}, too much deviation"
2834            );
2835
2836            // Verify boundaries were not unnecessarily shifted
2837            for (_start, end) in partitions.iter().take(actual_n.saturating_sub(1)) {
2838                assert_eq!(data[end - 1], delim);
2839            }
2840        }
2841    }
2842
2843    #[test]
2844    fn partition_balance_variable_records() {
2845        let delim = b'\n';
2846        let pattern: &[usize] = &[20, 100, 4096]; // 20B, 100B, 4KiB repeating
2847        let mut data = Vec::new();
2848        let mut i = 0;
2849        while data.len() < 1_000_000 {
2850            let payload_size = pattern[i % pattern.len()].saturating_sub(1);
2851            let fill = (i as u8).wrapping_add(0x41);
2852            data.extend(std::iter::repeat(fill).take(payload_size));
2853            data.push(delim);
2854            i += 1;
2855        }
2856        let file_len = data.len();
2857
2858        for &n in &[2, 4, 8, 16, 32] {
2859            let partitions = find_partition_boundaries(&data, n, delim);
2860            let actual_n = partitions.len();
2861            let _ideal = file_len / actual_n;
2862
2863            // With max record of 4KB, deviation should stay bounded
2864            for (start, end) in &partitions {
2865                let size = end - start;
2866                // Extremely loose bound — just ensure no catastrophic imbalance
2867                assert!(size > 0, "zero-length partition at n={n}");
2868            }
2869
2870            // Verify coverage
2871            let total: usize = partitions.iter().map(|(s, e)| e - s).sum();
2872            assert_eq!(total, file_len);
2873        }
2874    }
2875
2876    #[test]
2877    fn partition_absolute_vs_iterative_drift() {
2878        // This test verifies that the absolute-target algorithm does NOT
2879        // suffer from cumulative drift like the iterative approach.
2880        //
2881        // For a file with one large record (delaying the first boundary),
2882        // the iterative approach would shift ALL subsequent boundaries
2883        // by the overshoot. Absolute targets recenter.
2884
2885        let delim = b'\n';
2886        let mut data = Vec::new();
2887        // One giant record at the start
2888        data.extend_from_slice(b"giant_record_with_no_delimiter_here");
2889        // Then many small records
2890        for i in 0..100usize {
2891            let record = format!("line-{i:04}\n").into_bytes();
2892            data.extend_from_slice(&record);
2893        }
2894        let file_len = data.len();
2895        let n = 8;
2896
2897        let partitions = find_partition_boundaries(&data, n, delim);
2898        let actual_n = partitions.len();
2899
2900        // The first partition should contain the giant record
2901        assert_eq!(partitions[0].0, 0);
2902        // Remaining partitions should each be approximately equal
2903        let remaining_data = file_len - partitions[0].1;
2904        let remaining_partitions = actual_n - 1;
2905        let ideal_remaining = remaining_data / remaining_partitions;
2906
2907        let max_dev: usize = partitions[1..]
2908            .iter()
2909            .map(|(s, e)| {
2910                let size = e - s;
2911                size.abs_diff(ideal_remaining)
2912            })
2913            .max()
2914            .unwrap_or(0);
2915
2916        // After the giant record, remaining partitions should be well-balanced
2917        // (bounded by max record size, which is small here)
2918        let max_record = b"giant_record_with_no_delimiter_here".len() + 1 + 10; // generous
2919        assert!(
2920            max_dev <= max_record * 2 + 50,
2921            "remaining partitions unbalanced: max_dev={max_dev}, ideal_remaining={ideal_remaining}"
2922        );
2923
2924        // Also verify: the giant record is NOT split
2925        assert!(partitions.len() <= actual_n);
2926    }
2927
2928    #[test]
2929    fn partition_sparse_64k_records() {
2930        let delim = b'\n';
2931        let record_size = 65536;
2932        let record_count = 50;
2933        let mut data = Vec::with_capacity(record_count * record_size);
2934        for i in 0..record_count {
2935            let fill = (i as u8).wrapping_add(0x41);
2936            data.extend(std::iter::repeat(fill).take(record_size - 1));
2937            data.push(delim);
2938        }
2939        let file_len = data.len();
2940
2941        for &n in &[2, 4, 8, 16] {
2942            let partitions = find_partition_boundaries(&data, n, delim);
2943
2944            // Complete coverage
2945            let total: usize = partitions.iter().map(|(s, e)| e - s).sum();
2946            assert_eq!(total, file_len);
2947
2948            // With 64KB records, partitions may be few. There should be no split records.
2949            for (_start, end) in partitions.iter().take(partitions.len().saturating_sub(1)) {
2950                // Non-final partitions end after a delimiter
2951                assert_eq!(data[end.wrapping_sub(1)], delim, "64KB record split? n={n}");
2952            }
2953        }
2954    }
2955
2956    #[test]
2957    fn partition_no_empty_to_hit_n() {
2958        // A file with 2 records, N=100 -> should produce 2 partitions, not 100
2959        let data = b"record1\nrecord2\n";
2960        let partitions = find_partition_boundaries(data, 100, b'\n');
2961        assert!(!partitions.is_empty());
2962        for &(s, e) in &partitions {
2963            assert!(e > s, "must not create empty partitions");
2964        }
2965        assert!(
2966            partitions.len() < 100,
2967            "should produce fewer partitions than N"
2968        );
2969    }
2970
2971    #[test]
2972    fn partition_consecutive_delimiters() {
2973        let data = b"\n\n\n\n\n";
2974        for &n in &[2, 3, 4] {
2975            let partitions = find_partition_boundaries(data, n, b'\n');
2976            if partitions.is_empty() {
2977                continue;
2978            }
2979            assert_eq!(partitions[0].0, 0);
2980            for i in 1..partitions.len() {
2981                assert_eq!(partitions[i].0, partitions[i - 1].1);
2982            }
2983            assert_eq!(partitions.last().unwrap().1, data.len());
2984        }
2985    }
2986
2987    #[test]
2988    fn partition_only_newlines() {
2989        let data = b"\n\n\n";
2990        for &n in &[1, 2, 3, 4] {
2991            let partitions = find_partition_boundaries(data, n, b'\n');
2992            let total: usize = partitions.iter().map(|(s, e)| e - s).sum();
2993            assert_eq!(total, data.len());
2994        }
2995    }
2996
2997    // ── Multi-byte delimiter tests ──────────────────────────────────────
2998
2999    /// Verify pattern scanner == single-byte scanner for 1-byte delimiters.
3000    fn pattern_equals_single_byte(data: &[u8], chunk_size: usize, delimiter: u8) {
3001        let single = find_chunk_boundaries(data, chunk_size, delimiter);
3002        let pattern = find_chunk_boundaries_pattern(data, chunk_size, &[delimiter]);
3003        assert_eq!(
3004            single, pattern,
3005            "pattern != single: chunk_size={chunk_size} delim={delimiter:#04x}"
3006        );
3007    }
3008
3009    /// Verify PatternChunkCursor == ChunkCursor for 1-byte delimiters.
3010    fn cursor_pattern_equals_single_byte(data: &[u8], chunk_size: usize, delimiter: u8) {
3011        let single: Vec<&[u8]> = ChunkCursor::new(data, chunk_size, delimiter).collect();
3012        let pattern: Vec<&[u8]> = PatternChunkCursor::new(data, chunk_size, &[delimiter]).collect();
3013        assert_eq!(
3014            single, pattern,
3015            "pattern cursor != single cursor: chunk_size={chunk_size} delim={delimiter:#04x}",
3016        );
3017    }
3018
3019    #[test]
3020    fn pattern_single_byte_equivalence_all_delimiters() {
3021        let cases: &[(&[u8], usize, u8)] = &[
3022            (b"hello\nworld\n", 4, b'\n'),
3023            (b"a,b,c,d", 2, b','),
3024            (b"one\ttwo\tthree", 4, b'\t'),
3025            (b"a|b|c|d|e|f", 3, b'|'),
3026            (b"x\x00y\x00z", 2, b'\x00'),
3027            (b"single", 1024, b'\n'),
3028            (b"\n\n\n\n\n", 1, b'\n'),
3029            (b"", 1024, b'\n'),
3030            (b"\n", 1, b'\n'),
3031            (b"a", 1024, b'\n'),
3032            (b"line1\nline2\nline3\nline4\nline5\n", 10, b'\n'),
3033            (b"no_newlines_here", 5, b'\n'),
3034            (b"xxxx\n", 5, b'\n'),
3035            (b"a\nb\nc\n", 1, b'\n'),
3036            (b"tiny\nverylongrecordwithnobreaksanywhere\nend\n", 6, b'\n'),
3037        ];
3038        for &(data, chunk_size, delim) in cases {
3039            pattern_equals_single_byte(data, chunk_size, delim);
3040            cursor_pattern_equals_single_byte(data, chunk_size, delim);
3041        }
3042    }
3043
3044    #[test]
3045    fn pattern_empty_delimiter_panics() {
3046        let result = std::panic::catch_unwind(|| {
3047            find_chunk_boundaries_pattern(b"hello", 10, b"");
3048        });
3049        assert!(result.is_err(), "empty delimiter must panic");
3050    }
3051
3052    #[test]
3053    fn pattern_empty_data() {
3054        assert_eq!(find_chunk_boundaries_pattern(b"", 1024, b"\r\n"), vec![]);
3055        assert_eq!(
3056            PatternChunkCursor::new(b"", 1024, b"\r\n").collect::<Vec<_>>(),
3057            Vec::<&[u8]>::new()
3058        );
3059    }
3060
3061    #[test]
3062    fn pattern_crlf_basic() {
3063        let data = b"a\r\nb\r\nc\r\n";
3064        let chunks = find_chunk_boundaries_pattern(data, 4, b"\r\n");
3065        assert_eq!(chunks, vec![(0, 6), (6, 9)]);
3066
3067        let cursor: Vec<&[u8]> = PatternChunkCursor::new(data, 4, b"\r\n").collect();
3068        assert_eq!(cursor, vec![b"a\r\nb\r\n" as &[u8], b"c\r\n" as &[u8]]);
3069    }
3070
3071    #[test]
3072    fn pattern_crlf_no_trailing_delimiter() {
3073        let data = b"a\r\nb\r\nc";
3074        let chunks = find_chunk_boundaries_pattern(data, 1024, b"\r\n");
3075        assert_eq!(chunks, vec![(0, data.len())]);
3076    }
3077
3078    #[test]
3079    fn pattern_consecutive_crlf() {
3080        let data = b"\r\n\r\n\r\n";
3081        let chunks = find_chunk_boundaries_pattern(data, 1, b"\r\n");
3082        assert_eq!(chunks, vec![(0, 4), (4, 6)]);
3083    }
3084
3085    #[test]
3086    fn pattern_double_crlf_http_style() {
3087        let data = b"Header: val\r\n\r\nbody";
3088        let chunks = find_chunk_boundaries_pattern(data, 1024, b"\r\n\r\n");
3089        assert_eq!(chunks, vec![(0, 19)]);
3090    }
3091
3092    #[test]
3093    fn pattern_double_crlf_split_at_delimiter() {
3094        let data = b"Header: val\r\n\r\nbody line 2\r\n\r\nmore";
3095        let chunks = find_chunk_boundaries_pattern(data, 1, b"\r\n\r\n");
3096        assert_eq!(chunks.len(), 3);
3097        let total: usize = chunks.iter().map(|(s, e)| e - s).sum();
3098        assert_eq!(total, data.len());
3099        assert_eq!(&data[chunks[0].0..chunks[0].1], b"Header: val\r\n\r\n");
3100    }
3101
3102    #[test]
3103    fn pattern_custom_double_separator() {
3104        let data = b"a||b||c||d";
3105        let chunks = find_chunk_boundaries_pattern(data, 4, b"||");
3106        assert_eq!(chunks, vec![(0, 6), (6, 10)]);
3107
3108        let cursor: Vec<&[u8]> = PatternChunkCursor::new(data, 4, b"||").collect();
3109        assert_eq!(cursor, vec![b"a||b||" as &[u8], b"c||d" as &[u8]]);
3110    }
3111
3112    #[test]
3113    fn pattern_binary_delimiter() {
3114        let data = b"AB\x00\xFF\x00CD\x00\xFF\x00EF";
3115        let chunks = find_chunk_boundaries_pattern(data, 10, b"\x00\xff\x00");
3116        assert_eq!(chunks, vec![(0, data.len())]);
3117    }
3118
3119    #[test]
3120    fn pattern_binary_delimiter_small_chunk() {
3121        let data = b"AB\x00\xFF\x00CD\x00\xFF\x00EF";
3122        let chunks = find_chunk_boundaries_pattern(data, 4, b"\x00\xff\x00");
3123        assert_eq!(chunks, vec![(0, 10), (10, 12)]);
3124    }
3125
3126    #[test]
3127    fn pattern_at_eof() {
3128        let data = b"hello\r\nworld\r\n";
3129        let chunks = find_chunk_boundaries_pattern(data, 50, b"\r\n");
3130        assert_eq!(chunks, vec![(0, data.len())]);
3131    }
3132
3133    #[test]
3134    fn pattern_partial_at_eof() {
3135        let data = b"hello\r\nworld\r";
3136        let chunks = find_chunk_boundaries_pattern(data, 50, b"\r\n");
3137        assert_eq!(chunks, vec![(0, data.len())]);
3138    }
3139
3140    #[test]
3141    fn pattern_exactly_at_target() {
3142        let data = b"xxxx\r\n";
3143        let chunks = find_chunk_boundaries_pattern(data, 6, b"\r\n");
3144        assert_eq!(chunks, vec![(0, 6)]);
3145    }
3146
3147    #[test]
3148    fn pattern_starts_one_byte_before_target() {
3149        let data = b"xxxy\r\n";
3150        let chunks = find_chunk_boundaries_pattern(data, 6, b"\r\n");
3151        assert_eq!(chunks, vec![(0, data.len())]);
3152        assert_eq!(&data[..chunks[0].1], b"xxxy\r\n");
3153    }
3154
3155    #[test]
3156    fn pattern_no_delimiter() {
3157        let data = b"no_delimiter_here";
3158        let chunks = find_chunk_boundaries_pattern(data, 5, b"\r\n");
3159        assert_eq!(chunks, vec![(0, data.len())]);
3160    }
3161
3162    #[test]
3163    fn pattern_delimiter_longer_than_data() {
3164        let data = b"hi";
3165        let chunks = find_chunk_boundaries_pattern(data, 1024, b"\r\n\r\n\r\n");
3166        assert_eq!(chunks, vec![(0, data.len())]);
3167    }
3168
3169    #[test]
3170    fn pattern_chunk_size_zero() {
3171        let data = b"a\r\nb\r\nc\r\n";
3172        let chunks = find_chunk_boundaries_pattern(data, 0, b"\r\n");
3173        assert!(!chunks.is_empty());
3174        let total: usize = chunks.iter().map(|(s, e)| e - s).sum();
3175        assert_eq!(total, data.len());
3176    }
3177
3178    #[test]
3179    fn pattern_chunk_size_one() {
3180        let data = b"a\r\nb\r\nc\r\n";
3181        let chunks = find_chunk_boundaries_pattern(data, 1, b"\r\n");
3182        let mut pos = 0;
3183        for (start, end) in &chunks {
3184            assert_eq!(*start, pos);
3185            pos = *end;
3186        }
3187        assert_eq!(pos, data.len());
3188    }
3189
3190    #[test]
3191    fn pattern_huge_chunk_size() {
3192        let data = b"a\r\nb\r\nc\r\n";
3193        let chunks = find_chunk_boundaries_pattern(data, 1_000_000, b"\r\n");
3194        assert_eq!(chunks, vec![(0, data.len())]);
3195    }
3196
3197    #[test]
3198    fn pattern_overlapping_pattern() {
3199        // Delimiter "aa" should not match at position 0 in "aaa"
3200        let data = b"xaaaay";
3201        let chunks = find_chunk_boundaries_pattern(data, 1, b"aa");
3202        let total: usize = chunks.iter().map(|(s, e)| e - s).sum();
3203        assert_eq!(total, data.len());
3204    }
3205
3206    #[test]
3207    fn pattern_cursor_equivalence_crlf() {
3208        let data = b"line1\r\nline2\r\nline3\r\n";
3209        let eager = find_chunk_boundaries_pattern(data, 6, b"\r\n");
3210        let cursor: Vec<&[u8]> = PatternChunkCursor::new(data, 6, b"\r\n").collect();
3211        let lazy_ranges: Vec<(usize, usize)> = cursor
3212            .iter()
3213            .scan(0usize, |pos, &chunk| {
3214                let start = *pos;
3215                *pos += chunk.len();
3216                Some((start, *pos))
3217            })
3218            .collect();
3219        assert_eq!(lazy_ranges, eager);
3220    }
3221
3222    #[test]
3223    fn pattern_deterministic_corpus() {
3224        let mut data = Vec::new();
3225        for i in 0..1000u32 {
3226            data.extend_from_slice(format!("line_{i:04}\r\n").as_bytes());
3227        }
3228        let eager = find_chunk_boundaries_pattern(&data, 64, b"\r\n");
3229        let cursor: Vec<&[u8]> = PatternChunkCursor::new(&data, 64, b"\r\n").collect();
3230        let cursor_ranges: Vec<(usize, usize)> = cursor
3231            .iter()
3232            .scan(0usize, |pos, &chunk| {
3233                let start = *pos;
3234                *pos += chunk.len();
3235                Some((start, *pos))
3236            })
3237            .collect();
3238        assert_eq!(cursor_ranges, eager);
3239    }
3240
3241    #[test]
3242    fn pattern_property_no_gaps() {
3243        let data = b"rec1\r\nrec2\r\nrec3\r\nrec4\r\n";
3244        let chunks = find_chunk_boundaries_pattern(data, 6, b"\r\n");
3245        if chunks.is_empty() {
3246            return;
3247        }
3248        assert_eq!(chunks[0].0, 0);
3249        for i in 1..chunks.len() {
3250            assert_eq!(chunks[i].0, chunks[i - 1].1);
3251        }
3252        assert_eq!(chunks.last().unwrap().1, data.len());
3253    }
3254
3255    #[test]
3256    fn pattern_property_concatenation() {
3257        let cases: &[(&[u8], usize, &[u8])] = &[
3258            (b"a\r\nb\r\nc\r\n", 4, b"\r\n"),
3259            (b"ab||cd||ef", 4, b"||"),
3260            (b"single", 1024, b"\r\n"),
3261            (b"AB\xff\x00CD\xff\x00EF", 4, b"\xff\x00"),
3262        ];
3263        for &(data, cs, delim) in cases {
3264            let chunks = find_chunk_boundaries_pattern(data, cs, delim);
3265            let total: usize = chunks.iter().map(|(s, e)| e - s).sum();
3266            assert_eq!(total, data.len());
3267        }
3268    }
3269
3270    #[test]
3271    fn pattern_property_determinism() {
3272        let data = b"x||y||z||w||";
3273        let c1 = find_chunk_boundaries_pattern(data, 2, b"||");
3274        let c2 = find_chunk_boundaries_pattern(data, 2, b"||");
3275        assert_eq!(c1, c2);
3276    }
3277
3278    #[test]
3279    #[should_panic(expected = "delimiter must not be empty")]
3280    fn pattern_empty_delimiter_cursor_panics() {
3281        let _ = PatternChunkCursor::new(b"hello", 10, b"");
3282    }
3283
3284    #[test]
3285    fn pattern_empty_delimiter_cursor_panics_catch() {
3286        let result = std::panic::catch_unwind(|| {
3287            PatternChunkCursor::new(b"hello", 10, b"");
3288        });
3289        assert!(result.is_err());
3290    }
3291
3292    #[test]
3293    fn pattern_repeated_prefix_adversarial() {
3294        // Searching for "aaaaab" in "aaaaaaaaaa..." — worst-case for
3295        // first-byte SWAR + verify (hits many false positives).
3296        let delimiter = b"aaaaab";
3297        let hay = vec![b'a'; 10_000];
3298        let data: Vec<u8> = hay.iter().chain(delimiter.iter()).copied().collect();
3299        let chunks = find_chunk_boundaries_pattern(&data, 5000, delimiter);
3300        assert!(!chunks.is_empty());
3301        let total: usize = chunks.iter().map(|(s, e)| e - s).sum();
3302        assert_eq!(total, data.len());
3303    }
3304
3305    // ── Overflow safety — near-usize::MAX arithmetic ──────────────────
3306
3307    #[test]
3308    fn overflow_safe_partition_target_u128() {
3309        // On 64-bit, file_len * i can overflow u64. u128 prevents this.
3310        let huge: usize = usize::MAX;
3311        // file_len = huge, n = 3: product hits 2*huge which overflows u64
3312        // but fits in u128. Cannot actually mmap this, so test the formula.
3313        let target = (huge as u128) * (2u128) / (3u128);
3314        assert!(target < huge as u128);
3315
3316        // Verify the partition function produces correct boundaries at the
3317        // realistic scale: a real file fits in usize but needs correct math.
3318        let data = b"aa\nbb\ncc\ndd\nee\n";
3319        let partitions = find_partition_boundaries(data, 3, b'\n');
3320        let total: usize = partitions.iter().map(|(s, e)| e - s).sum();
3321        assert_eq!(total, data.len());
3322    }
3323
3324    #[test]
3325    fn overflow_safe_scanner_target_saturates() {
3326        // chunk_size near usize::MAX, start near len → saturating_add
3327        // prevents wrap. The test verifies the clamped behavior.
3328        let data = b"hello\nworld\n";
3329        let chunks = find_chunk_boundaries(data, usize::MAX, b'\n');
3330        assert_eq!(chunks, vec![(0, data.len())]);
3331    }
3332
3333    #[test]
3334    fn overflow_safe_cursor_target_saturates() {
3335        let data = b"hello\nworld\n";
3336        let cursor: Vec<&[u8]> = ChunkCursor::new(data, usize::MAX, b'\n').collect();
3337        let total: usize = cursor.iter().map(|c| c.len()).sum();
3338        assert_eq!(total, data.len());
3339    }
3340
3341    #[test]
3342    fn overflow_safe_pattern_target_saturates() {
3343        let data = b"a\r\nb\r\nc\r\n";
3344        let chunks = find_chunk_boundaries_pattern(data, usize::MAX, b"\r\n");
3345        assert_eq!(chunks, vec![(0, data.len())]);
3346    }
3347
3348    #[test]
3349    fn overflow_safe_pattern_cursor_target_saturates() {
3350        let data = b"a\r\nb\r\nc\r\n";
3351        let cursor: Vec<&[u8]> = PatternChunkCursor::new(data, usize::MAX, b"\r\n").collect();
3352        let total: usize = cursor.iter().map(|c| c.len()).sum();
3353        assert_eq!(total, data.len());
3354    }
3355
3356    #[test]
3357    fn overflow_safe_fixed_bounds_large_values() {
3358        // Already uses saturating_mul/saturating_add — verify correctness
3359        // with extreme parameters
3360        assert_eq!(fixed_chunk_count(0, usize::MAX), 0);
3361        assert_eq!(fixed_chunk_count(1024, usize::MAX), 1);
3362        assert_eq!(fixed_chunk_bounds(1024, usize::MAX, 0), Some((0, 1024)));
3363
3364        // chunk_size = 1 on large file
3365        let len: usize = 1000;
3366        assert_eq!(fixed_chunk_count(len, 1), len);
3367        assert_eq!(fixed_chunk_bounds(len, 1, len - 1), Some((len - 1, len)));
3368
3369        // Zero file, any chunk_size
3370        assert_eq!(fixed_chunk_bounds(0, usize::MAX, 0), None);
3371    }
3372}