Skip to main content

datum/io/
framing.rs

1use crate::stream::{BoxStream, Flow, NotUsed};
2use crate::{StreamError, StreamResult};
3
4/// Byte order used by [`Framing::length_field`] to interpret the length header.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum FramingByteOrder {
7    /// Most-significant byte first.
8    BigEndian,
9    /// Least-significant byte first.
10    LittleEndian,
11}
12
13#[derive(Clone)]
14enum Terminal {
15    Complete,
16    Error(StreamError),
17}
18
19fn sticky_terminal<T>(terminal: &Terminal) -> Option<StreamResult<T>> {
20    match terminal {
21        Terminal::Complete => None,
22        Terminal::Error(error) => Some(Err(error.clone())),
23    }
24}
25
26/// Byte-stream framing flows: turn a stream of arbitrary `Vec<u8>` chunks into a stream of
27/// complete logical frames. Mirrors Akka's `Framing`/`JsonFraming`.
28pub struct Framing;
29
30impl Framing {
31    /// Splits the byte stream on `delimiter`, stripping the delimiter from each emitted frame.
32    ///
33    /// Fails the stream once `maximum_frame_length` bytes accumulate without a delimiter. When the
34    /// stream ends mid-frame, `allow_truncation = true` emits the trailing bytes as a final frame;
35    /// `false` fails with [`StreamError`]. Panics if `delimiter` is empty or
36    /// `maximum_frame_length == 0`.
37    #[must_use]
38    pub fn delimiter(
39        delimiter: Vec<u8>,
40        maximum_frame_length: usize,
41        allow_truncation: bool,
42    ) -> Flow<Vec<u8>, Vec<u8>> {
43        assert!(
44            !delimiter.is_empty(),
45            "delimiter must contain at least one byte"
46        );
47        assert!(
48            maximum_frame_length > 0,
49            "maximum frame length must be greater than zero"
50        );
51        Flow::from_transform(move |input| {
52            Box::new(DelimiterFramingStream::new(
53                input,
54                delimiter.clone(),
55                maximum_frame_length,
56                allow_truncation,
57            )) as BoxStream<Vec<u8>>
58        })
59    }
60
61    /// Splits length-prefixed frames. The header is `field_length` bytes (1–4) at `field_offset`,
62    /// interpreted per `byte_order`; the emitted frame includes the header and the payload.
63    ///
64    /// The decoded length is signed (`i32`, matching Akka): a 4-byte header with the high bit set
65    /// decodes negative and fails the stream. Fails when a decoded frame would exceed
66    /// `maximum_frame_length`. Panics if `field_length` is not in `1..=4` or
67    /// `maximum_frame_length == 0`.
68    #[must_use]
69    pub fn length_field(
70        field_length: usize,
71        field_offset: usize,
72        maximum_frame_length: usize,
73        byte_order: FramingByteOrder,
74    ) -> Flow<Vec<u8>, Vec<u8>> {
75        assert!(
76            (1..=4).contains(&field_length),
77            "Length field length must be 1, 2, 3 or 4."
78        );
79        assert!(
80            maximum_frame_length > 0,
81            "maximum frame length must be greater than zero"
82        );
83        Flow::from_transform(move |input| {
84            Box::new(LengthFieldFramingStream::new(
85                input,
86                field_length,
87                field_offset,
88                maximum_frame_length,
89                byte_order,
90            )) as BoxStream<Vec<u8>>
91        })
92    }
93
94    /// Extracts top-level JSON objects from a concatenated byte stream, handling objects split
95    /// across arbitrary chunk boundaries.
96    ///
97    /// Advances past outer array brackets and commas, so both `{...}{...}` and `[{...},{...}]`
98    /// inputs work. Fails the stream if a single object exceeds `maximum_object_length`. Panics if
99    /// `maximum_object_length == 0`.
100    #[must_use]
101    pub fn json(maximum_object_length: usize) -> Flow<Vec<u8>, Vec<u8>, NotUsed> {
102        assert!(
103            maximum_object_length > 0,
104            "maximum object length must be greater than zero"
105        );
106        Flow::from_transform(move |input| {
107            Box::new(JsonFramingStream::new(input, maximum_object_length)) as BoxStream<Vec<u8>>
108        })
109    }
110}
111
112struct DelimiterFramingStream {
113    input: BoxStream<Vec<u8>>,
114    delimiter: Vec<u8>,
115    maximum_frame_length: usize,
116    allow_truncation: bool,
117    buffer: Vec<u8>,
118    terminal: Option<Terminal>,
119}
120
121impl DelimiterFramingStream {
122    fn new(
123        input: BoxStream<Vec<u8>>,
124        delimiter: Vec<u8>,
125        maximum_frame_length: usize,
126        allow_truncation: bool,
127    ) -> Self {
128        Self {
129            input,
130            delimiter,
131            maximum_frame_length,
132            allow_truncation,
133            buffer: Vec::new(),
134            terminal: None,
135        }
136    }
137
138    fn fail<T>(&mut self, error: StreamError) -> Option<StreamResult<T>> {
139        self.terminal = Some(Terminal::Error(error.clone()));
140        Some(Err(error))
141    }
142
143    fn delimiter_position(&self) -> Option<usize> {
144        self.buffer
145            .windows(self.delimiter.len())
146            .position(|window| window == self.delimiter.as_slice())
147    }
148
149    fn trailing_delimiter_prefix_len(&self) -> usize {
150        let max_prefix = self
151            .delimiter
152            .len()
153            .saturating_sub(1)
154            .min(self.buffer.len());
155        (1..=max_prefix)
156            .rev()
157            .find(|&prefix_len| self.buffer.ends_with(&self.delimiter[..prefix_len]))
158            .unwrap_or(0)
159    }
160}
161
162impl Iterator for DelimiterFramingStream {
163    type Item = StreamResult<Vec<u8>>;
164
165    fn next(&mut self) -> Option<Self::Item> {
166        if let Some(terminal) = &self.terminal {
167            return sticky_terminal(terminal);
168        }
169
170        loop {
171            if let Some(position) = self.delimiter_position() {
172                if position > self.maximum_frame_length {
173                    return self.fail(StreamError::Failed(format!(
174                        "Read {position} bytes which is more than {} without seeing a line terminator",
175                        self.maximum_frame_length
176                    )));
177                }
178                let frame = self.buffer[..position].to_vec();
179                self.buffer.drain(..position + self.delimiter.len());
180                return Some(Ok(frame));
181            }
182
183            // Match Akka's Framing delimiter stage here: a trailing partial delimiter
184            // (Framing.scala lines 262-284) may still complete on the next pull, so only
185            // bytes before that partial match count toward the max-length failure.
186            let trailing_partial = self.trailing_delimiter_prefix_len();
187            let unmatched = self.buffer.len() - trailing_partial;
188            if unmatched > self.maximum_frame_length {
189                return self.fail(StreamError::Failed(format!(
190                    "Read {} bytes which is more than {} without seeing a line terminator",
191                    unmatched, self.maximum_frame_length
192                )));
193            }
194
195            match self.input.next() {
196                Some(Ok(chunk)) => self.buffer.extend_from_slice(&chunk),
197                Some(Err(error)) => {
198                    self.terminal = Some(Terminal::Error(error.clone()));
199                    return Some(Err(error));
200                }
201                None => {
202                    if self.buffer.is_empty() {
203                        self.terminal = Some(Terminal::Complete);
204                        return None;
205                    }
206                    if self.allow_truncation {
207                        let frame = std::mem::take(&mut self.buffer);
208                        self.terminal = Some(Terminal::Complete);
209                        return Some(Ok(frame));
210                    }
211                    return self.fail(StreamError::Failed(
212                        "Stream finished but there was a truncated final frame in the buffer"
213                            .to_owned(),
214                    ));
215                }
216            }
217        }
218    }
219}
220
221struct LengthFieldFramingStream {
222    input: BoxStream<Vec<u8>>,
223    field_length: usize,
224    field_offset: usize,
225    minimum_chunk_size: usize,
226    maximum_frame_length: usize,
227    byte_order: FramingByteOrder,
228    buffer: Vec<u8>,
229    frame_size: Option<usize>,
230    terminal: Option<Terminal>,
231}
232
233impl LengthFieldFramingStream {
234    fn new(
235        input: BoxStream<Vec<u8>>,
236        field_length: usize,
237        field_offset: usize,
238        maximum_frame_length: usize,
239        byte_order: FramingByteOrder,
240    ) -> Self {
241        let minimum_chunk_size = field_offset + field_length;
242        Self {
243            input,
244            field_length,
245            field_offset,
246            minimum_chunk_size,
247            maximum_frame_length,
248            byte_order,
249            buffer: Vec::new(),
250            frame_size: None,
251            terminal: None,
252        }
253    }
254
255    fn fail<T>(&mut self, error: StreamError) -> Option<StreamResult<T>> {
256        self.terminal = Some(Terminal::Error(error.clone()));
257        Some(Err(error))
258    }
259
260    fn parse_length(&self) -> i32 {
261        let bytes = &self.buffer[self.field_offset..self.field_offset + self.field_length];
262        match (self.byte_order, self.field_length) {
263            (FramingByteOrder::BigEndian, 1) => i32::from(bytes[0]),
264            (FramingByteOrder::BigEndian, 2) => i32::from(u16::from_be_bytes([bytes[0], bytes[1]])),
265            (FramingByteOrder::BigEndian, 3) => {
266                ((i32::from(bytes[0])) << 16) | ((i32::from(bytes[1])) << 8) | i32::from(bytes[2])
267            }
268            (FramingByteOrder::BigEndian, 4) => {
269                i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
270            }
271            (FramingByteOrder::LittleEndian, 1) => i32::from(bytes[0]),
272            (FramingByteOrder::LittleEndian, 2) => {
273                i32::from(u16::from_le_bytes([bytes[0], bytes[1]]))
274            }
275            (FramingByteOrder::LittleEndian, 3) => {
276                ((i32::from(bytes[2])) << 16) | ((i32::from(bytes[1])) << 8) | i32::from(bytes[0])
277            }
278            (FramingByteOrder::LittleEndian, 4) => {
279                i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
280            }
281            _ => unreachable!("field length validated at construction"),
282        }
283    }
284}
285
286impl Iterator for LengthFieldFramingStream {
287    type Item = StreamResult<Vec<u8>>;
288
289    fn next(&mut self) -> Option<Self::Item> {
290        if let Some(terminal) = &self.terminal {
291            return sticky_terminal(terminal);
292        }
293
294        loop {
295            if let Some(frame_size) = self.frame_size {
296                if self.buffer.len() >= frame_size {
297                    let frame = self.buffer[..frame_size].to_vec();
298                    self.buffer.drain(..frame_size);
299                    self.frame_size = None;
300                    return Some(Ok(frame));
301                }
302            } else if self.buffer.len() >= self.minimum_chunk_size {
303                let parsed_length = self.parse_length();
304                if parsed_length < 0 {
305                    return self.fail(StreamError::Failed(format!(
306                        "Decoded frame header reported negative size {parsed_length}"
307                    )));
308                }
309                let frame_size = parsed_length as usize + self.minimum_chunk_size;
310                if frame_size > self.maximum_frame_length {
311                    return self.fail(StreamError::Failed(format!(
312                        "Maximum allowed frame size is {} but decoded frame header reported size {frame_size}",
313                        self.maximum_frame_length
314                    )));
315                }
316                if frame_size < self.minimum_chunk_size {
317                    return self.fail(StreamError::Failed(format!(
318                        "Computed frame size {frame_size} is less than minimum chunk size {}",
319                        self.minimum_chunk_size
320                    )));
321                }
322                self.frame_size = Some(frame_size);
323                continue;
324            }
325
326            match self.input.next() {
327                Some(Ok(chunk)) => self.buffer.extend_from_slice(&chunk),
328                Some(Err(error)) => {
329                    self.terminal = Some(Terminal::Error(error.clone()));
330                    return Some(Err(error));
331                }
332                None => {
333                    if self.buffer.is_empty() {
334                        self.terminal = Some(Terminal::Complete);
335                        return None;
336                    }
337                    return self.fail(StreamError::Failed(
338                        "Stream finished but there was a truncated final frame in the buffer"
339                            .to_owned(),
340                    ));
341                }
342            }
343        }
344    }
345}
346
347struct JsonFramingStream {
348    input: BoxStream<Vec<u8>>,
349    maximum_object_length: usize,
350    buffer: Vec<u8>,
351    pos: usize,
352    start: usize,
353    depth: usize,
354    completed_object: bool,
355    in_string_expression: bool,
356    in_backslash_escape: bool,
357    terminal: Option<Terminal>,
358}
359
360// Outer-level character class used by skip_to_next_object.
361const OUTER_OBJECT: u8 = 2; // b'{' — start of JSON object
362const OUTER_SKIP: u8 = 1; // b'[', b']', b',', whitespace — advance start
363const OUTER_ERROR: u8 = 0; // anything else — invalid JSON
364
365static OUTER_CHARS: [u8; 256] = {
366    let mut table = [OUTER_ERROR; 256];
367    table[b'{' as usize] = OUTER_OBJECT;
368    table[b'[' as usize] = OUTER_SKIP;
369    table[b']' as usize] = OUTER_SKIP;
370    table[b',' as usize] = OUTER_SKIP;
371    table[b' ' as usize] = OUTER_SKIP;
372    table[b'\n' as usize] = OUTER_SKIP;
373    table[b'\r' as usize] = OUTER_SKIP;
374    table[b'\t' as usize] = OUTER_SKIP;
375    table
376};
377
378// Inner-level "interesting" flag used by scan_object in non-string mode.
379// A byte is uninteresting (0) when it cannot change depth or enter a string:
380// anything other than `"`, `{`, or `}`. These bytes are skipped in a tight
381// inner loop without entering the full match, reducing branch pressure.
382static INNER_INTERESTING: [u8; 256] = {
383    let mut table = [0u8; 256];
384    table[b'"' as usize] = 1;
385    table[b'{' as usize] = 1;
386    table[b'}' as usize] = 1;
387    table
388};
389
390// String-mode "interesting" flag: only `"` (ends or is escaped by the prior
391// `\`) and `\` (starts escape). Every other byte in a string is a plain char
392// that carries no parser-state change when not in escape mode, so it can be
393// skipped without the `in_escape = false` store that the full match requires.
394static STRING_INTERESTING: [u8; 256] = {
395    let mut table = [0u8; 256];
396    table[b'"' as usize] = 1;
397    table[b'\\' as usize] = 1;
398    table
399};
400
401impl JsonFramingStream {
402    fn new(input: BoxStream<Vec<u8>>, maximum_object_length: usize) -> Self {
403        Self {
404            input,
405            maximum_object_length,
406            buffer: Vec::with_capacity(maximum_object_length.min(4096)),
407            pos: 0,
408            start: 0,
409            depth: 0,
410            completed_object: false,
411            in_string_expression: false,
412            in_backslash_escape: false,
413            terminal: None,
414        }
415    }
416
417    fn fail<T>(&mut self, error: StreamError) -> Option<StreamResult<T>> {
418        self.terminal = Some(Terminal::Error(error.clone()));
419        Some(Err(error))
420    }
421
422    fn can_complete(&self) -> bool {
423        self.depth == 0
424    }
425
426    /// Compact the buffer in place, discarding bytes before `start`.
427    /// Called before extending with new input when the current slice
428    /// would otherwise force a reallocation.
429    fn compact(&mut self) {
430        if self.start == 0 {
431            return;
432        }
433        if self.start >= self.buffer.len() {
434            self.buffer.clear();
435        } else {
436            self.buffer.drain(..self.start);
437        }
438        self.pos -= self.start;
439        self.start = 0;
440    }
441
442    fn skip_to_next_object(&mut self) -> StreamResult<()> {
443        if self.depth > 0 {
444            return Ok(());
445        }
446        let max = self.buffer.len();
447        let limit = self.maximum_object_length;
448        let mut pos = self.pos;
449        let mut start = self.start;
450        while pos < max && pos - start < limit {
451            let outer = OUTER_CHARS[self.buffer[pos] as usize];
452            if outer == OUTER_SKIP {
453                start += 1;
454                pos += 1;
455            } else if outer == OUTER_OBJECT {
456                self.start = start;
457                self.pos = pos + 1;
458                self.depth = 1;
459                return Ok(());
460            } else {
461                return Err(StreamError::Failed(format!(
462                    "Invalid JSON encountered at position [{}] of [{}]",
463                    self.start,
464                    String::from_utf8_lossy(&self.buffer)
465                )));
466            }
467        }
468        self.start = start;
469        self.pos = pos;
470        Ok(())
471    }
472
473    fn scan_object(&mut self) -> StreamResult<()> {
474        let max = self.buffer.len();
475        let limit = self.maximum_object_length;
476        let mut pos = self.pos;
477        let mut depth = self.depth;
478        let mut in_string = self.in_string_expression;
479        let mut in_escape = self.in_backslash_escape;
480        let start = self.start;
481        let mut completed = false;
482
483        while pos < max && pos - start < limit {
484            let byte = self.buffer[pos];
485            if in_string {
486                if in_escape {
487                    // The escaped character is always a single byte; consume
488                    // it unconditionally and reset escape state.
489                    in_escape = false;
490                } else if STRING_INTERESTING[byte as usize] != 0 {
491                    // Only `"` and `\` require state changes in string mode.
492                    if byte == b'"' {
493                        in_string = false;
494                    } else {
495                        in_escape = true;
496                    }
497                }
498                // Uninteresting string bytes (the vast majority) take no
499                // state-change path: no match, no store. This eliminates the
500                // `in_escape = false` write for every plain character in a
501                // string value, the dominant cost in the original inner scan.
502            } else {
503                // Fast-skip uninteresting bytes in non-string mode: anything
504                // that cannot change depth or enter string state.
505                if INNER_INTERESTING[byte as usize] == 0 {
506                    pos += 1;
507                    continue;
508                }
509                match byte {
510                    b'"' => in_string = true,
511                    b'{' => depth += 1,
512                    b'}' => {
513                        depth -= 1;
514                        if depth == 0 {
515                            pos += 1;
516                            completed = true;
517                            break;
518                        }
519                    }
520                    _ => {}
521                }
522            }
523            pos += 1;
524        }
525
526        self.pos = pos;
527        self.depth = depth;
528        self.in_string_expression = in_string;
529        self.in_backslash_escape = in_escape;
530        self.completed_object = completed;
531        Ok(())
532    }
533
534    fn poll_object(&mut self) -> StreamResult<Option<Vec<u8>>> {
535        self.completed_object = false;
536        self.skip_to_next_object()?;
537        self.scan_object()?;
538
539        if !self.completed_object
540            && self.pos.saturating_sub(self.start) >= self.maximum_object_length
541        {
542            return Err(StreamError::Failed(format!(
543                "JSON element exceeded maximumObjectLength ({} bytes)!",
544                self.maximum_object_length
545            )));
546        }
547
548        if self.completed_object && self.start < self.pos {
549            let frame = self.buffer[self.start..self.pos].to_vec();
550            // Advance start lazily; the actual drain (compact) is deferred
551            // until the next chunk pull would otherwise force a realloc.
552            self.start = self.pos;
553            return Ok(Some(frame));
554        }
555
556        Ok(None)
557    }
558}
559
560impl Iterator for JsonFramingStream {
561    type Item = StreamResult<Vec<u8>>;
562
563    fn next(&mut self) -> Option<Self::Item> {
564        if let Some(terminal) = &self.terminal {
565            return sticky_terminal(terminal);
566        }
567
568        loop {
569            match self.poll_object() {
570                Ok(Some(frame)) => return Some(Ok(frame)),
571                Ok(None) => {}
572                Err(error) => return self.fail(error),
573            }
574
575            match self.input.next() {
576                Some(Ok(chunk)) => {
577                    // Compact before extending only when the new chunk
578                    // would not fit in remaining buffer capacity. This
579                    // avoids a memmove on every chunk; the cost is paid
580                    // only when compaction is needed to prevent realloc.
581                    if self.start > 0 && self.buffer.capacity() - self.buffer.len() < chunk.len() {
582                        let tail = self.buffer.len() - self.start;
583                        if tail + chunk.len() <= self.buffer.capacity() {
584                            self.compact();
585                        }
586                    }
587                    self.buffer.extend_from_slice(&chunk);
588                }
589                Some(Err(error)) => {
590                    self.terminal = Some(Terminal::Error(error.clone()));
591                    return Some(Err(error));
592                }
593                None => {
594                    if self.start >= self.buffer.len() {
595                        self.terminal = Some(Terminal::Complete);
596                        return None;
597                    }
598                    if self.can_complete() {
599                        self.start = 0;
600                        self.terminal = Some(Terminal::Complete);
601                        return None;
602                    }
603                    return self.fail(StreamError::Failed(
604                        "Stream finished but there was a truncated final frame in the buffer"
605                            .to_owned(),
606                    ));
607                }
608            }
609        }
610    }
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616    use crate::testkit::{TestSink, TestSource};
617    use crate::{Keep, Source};
618
619    #[test]
620    fn delimiter_framing_handles_split_frames() {
621        let sink = Source::from_iter([b"ab|c".to_vec(), b"d|".to_vec()])
622            .via(Framing::delimiter(b"|".to_vec(), 16, false))
623            .run_with(TestSink::probe())
624            .expect("delimiter framing materializes");
625
626        sink.request(3);
627        sink.assert_next_n([b"ab".to_vec(), b"cd".to_vec()]);
628        sink.expect_complete();
629    }
630
631    #[test]
632    fn delimiter_framing_fails_on_max_length_exceeded() {
633        let sink = Source::single(b"abcdef".to_vec())
634            .via(Framing::delimiter(b"|".to_vec(), 3, false))
635            .run_with(TestSink::probe())
636            .expect("delimiter framing materializes");
637
638        sink.request(1);
639        assert_eq!(
640            sink.expect_error(),
641            StreamError::Failed(
642                "Read 6 bytes which is more than 3 without seeing a line terminator".to_owned()
643            )
644        );
645    }
646
647    #[test]
648    fn delimiter_framing_accepts_max_length_frame_with_split_partial_delimiter() {
649        let frame = vec![b'a'; 64];
650        let first = [frame.clone(), vec![b'\r']].concat();
651        let second = vec![b'\n'];
652        let sink = Source::from_iter([first, second])
653            .via(Framing::delimiter(b"\r\n".to_vec(), 64, false))
654            .run_with(TestSink::probe())
655            .expect("delimiter framing materializes");
656
657        sink.request(2);
658        sink.assert_next(frame);
659        sink.expect_complete();
660    }
661
662    #[test]
663    fn delimiter_framing_still_fails_when_max_length_is_exceeded_without_partial_match() {
664        let sink = Source::single(vec![b'a'; 65])
665            .via(Framing::delimiter(b"\r\n".to_vec(), 64, false))
666            .run_with(TestSink::probe())
667            .expect("delimiter framing materializes");
668
669        sink.request(1);
670        assert_eq!(
671            sink.expect_error(),
672            StreamError::Failed(
673                "Read 65 bytes which is more than 64 without seeing a line terminator".to_owned()
674            )
675        );
676    }
677
678    #[test]
679    fn delimiter_framing_fails_on_truncated_eof() {
680        let sink = Source::single(b"abc".to_vec())
681            .via(Framing::delimiter(b"|".to_vec(), 8, false))
682            .run_with(TestSink::probe())
683            .expect("delimiter framing materializes");
684
685        sink.request(1);
686        assert_eq!(
687            sink.expect_error(),
688            StreamError::Failed(
689                "Stream finished but there was a truncated final frame in the buffer".to_owned()
690            )
691        );
692    }
693
694    #[test]
695    fn delimiter_framing_allows_truncation_when_enabled() {
696        let sink = Source::single(b"abc".to_vec())
697            .via(Framing::delimiter(b"|".to_vec(), 8, true))
698            .run_with(TestSink::probe())
699            .expect("delimiter framing materializes");
700
701        sink.request(2);
702        sink.assert_next(b"abc".to_vec());
703        sink.expect_complete();
704    }
705
706    #[test]
707    fn length_field_framing_handles_split_headers_and_payloads() {
708        let frame = [0_u8, 0_u8, 0_u8, 2_u8, b'o', b'k'];
709        let sink = Source::from_iter([frame[..3].to_vec(), frame[3..].to_vec()])
710            .via(Framing::length_field(4, 0, 16, FramingByteOrder::BigEndian))
711            .run_with(TestSink::probe())
712            .expect("length field framing materializes");
713
714        sink.request(2);
715        sink.assert_next(frame.to_vec());
716        sink.expect_complete();
717    }
718
719    #[test]
720    fn length_field_framing_reads_header_at_non_zero_offset() {
721        let frame = [b'i', b'd', 0_u8, 3_u8, b'h', b'e', b'y'];
722        let sink = Source::from_iter([
723            frame[..1].to_vec(),
724            frame[1..3].to_vec(),
725            frame[3..].to_vec(),
726        ])
727        .via(Framing::length_field(2, 2, 16, FramingByteOrder::BigEndian))
728        .run_with(TestSink::probe())
729        .expect("length field framing materializes");
730
731        sink.request(2);
732        sink.assert_next(frame.to_vec());
733        sink.expect_complete();
734    }
735
736    #[test]
737    fn length_field_framing_fails_on_max_length_exceeded() {
738        let sink = Source::single(vec![0_u8, 0_u8, 0_u8, 8_u8])
739            .via(Framing::length_field(4, 0, 6, FramingByteOrder::BigEndian))
740            .run_with(TestSink::probe())
741            .expect("length field framing materializes");
742
743        sink.request(1);
744        assert_eq!(
745            sink.expect_error(),
746            StreamError::Failed(
747                "Maximum allowed frame size is 6 but decoded frame header reported size 12"
748                    .to_owned()
749            )
750        );
751    }
752
753    #[test]
754    fn length_field_framing_fails_on_truncated_eof() {
755        let sink = Source::single(vec![0_u8, 0_u8, 0_u8, 2_u8, b'o'])
756            .via(Framing::length_field(4, 0, 16, FramingByteOrder::BigEndian))
757            .run_with(TestSink::probe())
758            .expect("length field framing materializes");
759
760        sink.request(1);
761        assert_eq!(
762            sink.expect_error(),
763            StreamError::Failed(
764                "Stream finished but there was a truncated final frame in the buffer".to_owned()
765            )
766        );
767    }
768
769    #[test]
770    fn length_field_framing_treats_two_byte_big_endian_lengths_as_unsigned() {
771        let payload = vec![b'x'; 0x8000];
772        let mut frame = vec![0x80, 0x00];
773        frame.extend_from_slice(&payload);
774        let sink = Source::single(frame.clone())
775            .via(Framing::length_field(
776                2,
777                0,
778                frame.len(),
779                FramingByteOrder::BigEndian,
780            ))
781            .run_with(TestSink::probe())
782            .expect("length field framing materializes");
783
784        sink.request(2);
785        sink.assert_next(frame);
786        sink.expect_complete();
787    }
788
789    #[test]
790    fn length_field_framing_treats_two_byte_little_endian_lengths_as_unsigned() {
791        let payload = vec![b'y'; 0x8000];
792        let mut frame = vec![0x00, 0x80];
793        frame.extend_from_slice(&payload);
794        let sink = Source::single(frame.clone())
795            .via(Framing::length_field(
796                2,
797                0,
798                frame.len(),
799                FramingByteOrder::LittleEndian,
800            ))
801            .run_with(TestSink::probe())
802            .expect("length field framing materializes");
803
804        sink.request(2);
805        sink.assert_next(frame);
806        sink.expect_complete();
807    }
808
809    #[test]
810    fn length_field_framing_reads_three_byte_big_endian_lengths() {
811        let frame = [0_u8, 0_u8, 2_u8, b'o', b'k'];
812        let sink = Source::single(frame.to_vec())
813            .via(Framing::length_field(3, 0, 8, FramingByteOrder::BigEndian))
814            .run_with(TestSink::probe())
815            .expect("length field framing materializes");
816
817        sink.request(2);
818        sink.assert_next(frame.to_vec());
819        sink.expect_complete();
820    }
821
822    #[test]
823    fn length_field_framing_reads_three_byte_little_endian_lengths() {
824        let frame = [2_u8, 0_u8, 0_u8, b'o', b'k'];
825        let sink = Source::single(frame.to_vec())
826            .via(Framing::length_field(
827                3,
828                0,
829                8,
830                FramingByteOrder::LittleEndian,
831            ))
832            .run_with(TestSink::probe())
833            .expect("length field framing materializes");
834
835        sink.request(2);
836        sink.assert_next(frame.to_vec());
837        sink.expect_complete();
838    }
839
840    #[test]
841    fn length_field_framing_keeps_four_byte_signed_overflow_behavior() {
842        let sink = Source::single(vec![0x80, 0x00, 0x00, 0x00])
843            .via(Framing::length_field(
844                4,
845                0,
846                usize::MAX,
847                FramingByteOrder::BigEndian,
848            ))
849            .run_with(TestSink::probe())
850            .expect("length field framing materializes");
851
852        sink.request(1);
853        assert_eq!(
854            sink.expect_error(),
855            StreamError::Failed(
856                "Decoded frame header reported negative size -2147483648".to_owned()
857            )
858        );
859    }
860
861    #[test]
862    fn json_framing_extracts_objects_split_across_chunks() {
863        let sink = Source::from_iter([b"[{\"a\":1},".to_vec(), b"{\"b\":2}]".to_vec()])
864            .via(Framing::json(64))
865            .run_with(TestSink::probe())
866            .expect("json framing materializes");
867
868        sink.request(3);
869        sink.assert_next_n([b"{\"a\":1}".to_vec(), b"{\"b\":2}".to_vec()]);
870        sink.expect_complete();
871    }
872
873    #[test]
874    fn json_framing_fails_on_max_length_exceeded() {
875        let sink = Source::single(b"{\"abcdef\":1}".to_vec())
876            .via(Framing::json(4))
877            .run_with(TestSink::probe())
878            .expect("json framing materializes");
879
880        sink.request(1);
881        assert_eq!(
882            sink.expect_error(),
883            StreamError::Failed("JSON element exceeded maximumObjectLength (4 bytes)!".to_owned())
884        );
885    }
886
887    #[test]
888    fn json_framing_accepts_object_one_byte_under_max_length() {
889        let sink = Source::single(br#"{"":1}"#.to_vec())
890            .via(Framing::json(7))
891            .run_with(TestSink::probe())
892            .expect("json framing materializes");
893
894        sink.request(2);
895        sink.assert_next(br#"{"":1}"#.to_vec());
896        sink.expect_complete();
897    }
898
899    #[test]
900    fn json_framing_accepts_object_at_max_length() {
901        let sink = Source::single(br#"{"a":1}"#.to_vec())
902            .via(Framing::json(7))
903            .run_with(TestSink::probe())
904            .expect("json framing materializes");
905
906        sink.request(2);
907        sink.assert_next(br#"{"a":1}"#.to_vec());
908        sink.expect_complete();
909    }
910
911    #[test]
912    fn json_framing_rejects_object_one_byte_over_max_length() {
913        let sink = Source::single(br#"{"aa":1}"#.to_vec())
914            .via(Framing::json(7))
915            .run_with(TestSink::probe())
916            .expect("json framing materializes");
917
918        sink.request(1);
919        assert_eq!(
920            sink.expect_error(),
921            StreamError::Failed("JSON element exceeded maximumObjectLength (7 bytes)!".to_owned())
922        );
923    }
924
925    #[test]
926    fn json_framing_fails_on_truncated_eof() {
927        let sink = Source::single(b"{\"a\":".to_vec())
928            .via(Framing::json(32))
929            .run_with(TestSink::probe())
930            .expect("json framing materializes");
931
932        sink.request(1);
933        assert_eq!(
934            sink.expect_error(),
935            StreamError::Failed(
936                "Stream finished but there was a truncated final frame in the buffer".to_owned()
937            )
938        );
939    }
940
941    #[test]
942    fn framing_preserves_upstream_errors() {
943        let (source, sink) = TestSource::probe::<Vec<u8>>()
944            .via(Framing::delimiter(b"|".to_vec(), 32, false))
945            .to_mat(TestSink::probe(), Keep::both)
946            .run()
947            .expect("probe framing materializes");
948
949        sink.request(1);
950        source.send_error(StreamError::Failed("boom".to_owned()));
951        assert_eq!(sink.expect_error(), StreamError::Failed("boom".to_owned()));
952    }
953
954    #[test]
955    fn json_framing_no_bleed_across_buffer_reuse() {
956        use crate::Sink;
957        let mut payload = Vec::new();
958        payload.push(b'[');
959        for index in 0..128_usize {
960            if index > 0 {
961                payload.push(b',');
962            }
963            payload
964                .extend_from_slice(format!("{{\"id\":{index},\"pad\":\"xxxxxxxx\"}}").as_bytes());
965        }
966        payload.push(b']');
967
968        let chunks: Vec<Vec<u8>> = payload.chunks(193).map(|c| c.to_vec()).collect();
969        assert!(
970            chunks.len() > 4,
971            "chunks must split objects across boundaries"
972        );
973
974        let frames = Source::from_iter(chunks)
975            .via(Framing::json(256))
976            .run_with(Sink::collect())
977            .expect("json framing materializes")
978            .wait()
979            .expect("json framing completes");
980
981        assert_eq!(frames.len(), 128);
982        for (index, frame) in frames.iter().enumerate() {
983            let text = std::str::from_utf8(frame).expect("frame is valid utf-8");
984            let needle = format!("\"id\":{index}");
985            assert!(
986                text.contains(&needle),
987                "frame {index} missing {needle}: {text}"
988            );
989            assert_eq!(text.as_bytes()[0], b'{');
990            assert_eq!(text.as_bytes()[text.len() - 1], b'}');
991        }
992    }
993
994    #[test]
995    fn json_framing_compacts_buffer_at_chunk_boundaries() {
996        use crate::Sink;
997        let object = br#"{"id":1,"name":"x"}"#;
998        let mut payload = Vec::new();
999        payload.push(b'[');
1000        for _ in 0..32 {
1001            payload.push(b',');
1002            payload.extend_from_slice(object);
1003        }
1004        payload.push(b']');
1005        let chunks: Vec<Vec<u8>> = payload.iter().copied().map(|b| vec![b]).collect();
1006
1007        let frames = Source::from_iter(chunks)
1008            .via(Framing::json(64))
1009            .run_with(Sink::collect())
1010            .expect("json framing materializes")
1011            .wait()
1012            .expect("json framing completes");
1013
1014        assert_eq!(frames.len(), 32);
1015        for frame in &frames {
1016            assert_eq!(frame.as_slice(), object);
1017        }
1018    }
1019}