1pub(crate) const FRAME_FORMAT_V1: u8 = 1;
53
54pub(crate) const FRAME_FORMAT_V2: u8 = 2;
59
60const PAYLOAD_CONTAINER_SENTINEL: u8 = 0x89;
63const PAYLOAD_CONTAINER_MAGIC: &[u8; 4] = b"RJP\0";
64const PAYLOAD_CONTAINER_V1: u8 = 1;
65const PAYLOAD_CONTAINER_HEADER_LEN: usize = 1 + PAYLOAD_CONTAINER_MAGIC.len() + 1 + 4;
66
67pub(crate) const MAX_CONTAINER_CHUNKS: usize = 65_536;
73
74#[allow(dead_code)]
98pub(crate) fn encode_payload_chunks(chunks: &[&[u8]]) -> Result<Vec<u8>, PayloadEncodeError> {
99 let mut out = Vec::with_capacity(payload_container_len(chunks)?);
100 encode_payload_chunks_into(chunks, &mut out).map(|()| out)
101}
102
103fn payload_container_len(chunks: &[&[u8]]) -> Result<usize, PayloadEncodeError> {
106 if chunks.is_empty() {
107 return Err(PayloadEncodeError::Empty);
108 }
109 if chunks.len() > MAX_CONTAINER_CHUNKS {
110 return Err(PayloadEncodeError::TooManyChunks(chunks.len()));
111 }
112 let mut len = PAYLOAD_CONTAINER_HEADER_LEN;
113 for chunk in chunks {
114 len = payload_container_len_step(len, chunk.len())?;
115 }
116 Ok(len)
117}
118
119fn payload_container_len_step(
123 current: usize,
124 chunk_len: usize,
125) -> Result<usize, PayloadEncodeError> {
126 if chunk_len == 0 {
127 return Err(PayloadEncodeError::EmptyChunk);
128 }
129 u32::try_from(chunk_len).map_err(|_| PayloadEncodeError::ChunkTooLarge(chunk_len))?;
130 current
131 .checked_add(4)
132 .and_then(|n| n.checked_add(chunk_len))
133 .ok_or(PayloadEncodeError::ContainerTooLarge)
134}
135
136fn encode_payload_chunks_into(
138 chunks: &[&[u8]],
139 out: &mut Vec<u8>,
140) -> Result<(), PayloadEncodeError> {
141 payload_container_len(chunks)?;
142 out.push(PAYLOAD_CONTAINER_SENTINEL);
143 out.extend_from_slice(PAYLOAD_CONTAINER_MAGIC);
144 out.push(PAYLOAD_CONTAINER_V1);
145 out.extend_from_slice(&(chunks.len() as u32).to_le_bytes());
146 for chunk in chunks {
147 out.extend_from_slice(&(chunk.len() as u32).to_le_bytes());
148 out.extend_from_slice(chunk);
149 }
150 Ok(())
151}
152
153pub(crate) fn decode_payload_chunks(payload: &[u8]) -> Result<Vec<&[u8]>, PayloadDecodeError> {
159 if payload.first().copied() != Some(PAYLOAD_CONTAINER_SENTINEL) {
160 return Err(PayloadDecodeError::MissingSentinel);
161 }
162 if payload.len() < PAYLOAD_CONTAINER_HEADER_LEN {
163 return Err(PayloadDecodeError::Truncated);
164 }
165 if &payload[1..1 + PAYLOAD_CONTAINER_MAGIC.len()] != PAYLOAD_CONTAINER_MAGIC {
166 return Err(PayloadDecodeError::InvalidMagic);
167 }
168 let version_at = 1 + PAYLOAD_CONTAINER_MAGIC.len();
169 let version = payload[version_at];
170 if version != PAYLOAD_CONTAINER_V1 {
171 return Err(PayloadDecodeError::UnknownVersion(version));
172 }
173 let count_at = version_at + 1;
174 let count = u32::from_le_bytes(
175 payload[count_at..count_at + 4]
176 .try_into()
177 .expect("four-byte split"),
178 ) as usize;
179 if count == 0 {
180 return Err(PayloadDecodeError::Empty);
181 }
182 if count > MAX_CONTAINER_CHUNKS {
183 return Err(PayloadDecodeError::TooManyChunks(count));
184 }
185
186 let mut rest = &payload[PAYLOAD_CONTAINER_HEADER_LEN..];
187 if count > rest.len() / 4 {
190 return Err(PayloadDecodeError::Truncated);
191 }
192 let mut chunks = Vec::with_capacity(count);
193 for _ in 0..count {
194 if rest.len() < 4 {
195 return Err(PayloadDecodeError::Truncated);
196 }
197 let len = u32::from_le_bytes(rest[..4].try_into().expect("four-byte split")) as usize;
198 if len == 0 {
199 return Err(PayloadDecodeError::EmptyChunk);
200 }
201 rest = &rest[4..];
202 if rest.len() < len {
203 return Err(PayloadDecodeError::Truncated);
204 }
205 let (chunk, tail) = rest.split_at(len);
206 chunks.push(chunk);
207 rest = tail;
208 }
209 if !rest.is_empty() {
210 return Err(PayloadDecodeError::TrailingBytes(rest.len()));
211 }
212 Ok(chunks)
213}
214
215#[derive(Clone, Debug, PartialEq, Eq)]
216pub enum PayloadEncodeError {
217 Empty,
218 TooManyChunks(usize),
219 ChunkTooLarge(usize),
220 EmptyChunk,
222 ContainerTooLarge,
223}
224
225impl std::fmt::Display for PayloadEncodeError {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 match self {
228 PayloadEncodeError::Empty => {
229 write!(f, "a chunk container must contain at least one chunk")
230 }
231 PayloadEncodeError::TooManyChunks(n) => {
232 write!(
233 f,
234 "chunk count {n} exceeds the {MAX_CONTAINER_CHUNKS}-chunk container limit"
235 )
236 }
237 PayloadEncodeError::ChunkTooLarge(n) => {
238 write!(f, "chunk length {n} exceeds the u32 container limit")
239 }
240 PayloadEncodeError::EmptyChunk => {
241 write!(f, "a chunk container cannot hold a zero-length chunk")
242 }
243 PayloadEncodeError::ContainerTooLarge => {
244 write!(f, "chunk container length overflows addressable memory")
245 }
246 }
247 }
248}
249
250impl std::error::Error for PayloadEncodeError {}
251
252#[derive(Clone, Debug, PartialEq, Eq)]
253pub enum PayloadDecodeError {
254 MissingSentinel,
256 Truncated,
257 InvalidMagic,
258 UnknownVersion(u8),
259 Empty,
260 TooManyChunks(usize),
262 EmptyChunk,
264 TrailingBytes(usize),
265}
266
267impl std::fmt::Display for PayloadDecodeError {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 match self {
270 PayloadDecodeError::MissingSentinel => {
271 write!(f, "payload is not a chunk container (sentinel byte absent)")
272 }
273 PayloadDecodeError::Truncated => write!(f, "payload chunk container is truncated"),
274 PayloadDecodeError::InvalidMagic => {
275 write!(f, "payload chunk container has invalid magic")
276 }
277 PayloadDecodeError::UnknownVersion(v) => {
278 write!(f, "unknown payload chunk container version {v}")
279 }
280 PayloadDecodeError::Empty => write!(f, "payload chunk container has no chunks"),
281 PayloadDecodeError::TooManyChunks(n) => {
282 write!(
283 f,
284 "payload chunk container declares {n} chunks, over the \
285 {MAX_CONTAINER_CHUNKS}-chunk limit"
286 )
287 }
288 PayloadDecodeError::EmptyChunk => {
289 write!(f, "payload chunk container declares a zero-length chunk")
290 }
291 PayloadDecodeError::TrailingBytes(n) => {
292 write!(f, "payload chunk container has {n} trailing byte(s)")
293 }
294 }
295 }
296}
297
298impl std::error::Error for PayloadDecodeError {}
299
300#[derive(Clone, Copy, Debug, PartialEq, Eq)]
303pub enum FrameKind {
304 Rows = 0,
306 Ddl = 1,
310 Empty = 2,
316 }
321
322#[derive(Clone, Copy, Debug, PartialEq, Eq)]
325pub struct RunTotals {
326 pub run_rows: u64,
328 pub rows_cum: u64,
330}
331
332#[derive(Clone, Debug, PartialEq, Eq)]
334pub struct FrameHeader {
335 pub kind: FrameKind,
336 pub committed_at: i64,
341 pub run_id: Option<String>,
344 pub run_totals: Option<RunTotals>,
345}
346
347#[derive(Clone, Debug, PartialEq, Eq)]
350pub enum FrameEncodeError {
351 RunIdTooLong(usize),
353 EmptyFrameWithPayload,
357 Payload(PayloadEncodeError),
359}
360
361impl std::fmt::Display for FrameEncodeError {
362 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363 match self {
364 FrameEncodeError::RunIdTooLong(n) => {
365 write!(f, "run_id is {n} bytes; the length prefix caps it at 255")
366 }
367 FrameEncodeError::EmptyFrameWithPayload => {
368 write!(f, "an Empty frame must carry no payload")
369 }
370 FrameEncodeError::Payload(e) => write!(f, "invalid frame payload chunks: {e}"),
371 }
372 }
373}
374
375impl std::error::Error for FrameEncodeError {}
376
377#[derive(Clone, Debug, PartialEq, Eq)]
381pub enum FrameDecodeError {
382 Truncated,
384 UnknownFormat(u8),
385 UnknownKind(u8),
386 UnknownFlags(u8),
389 RunIdNotUtf8,
390 EmptyFrameWithPayload,
393 Payload(PayloadDecodeError),
395}
396
397impl std::fmt::Display for FrameDecodeError {
398 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399 match self {
400 FrameDecodeError::Truncated => write!(f, "frame truncated mid-header"),
401 FrameDecodeError::UnknownFormat(v) => write!(f, "unknown frame format version {v}"),
402 FrameDecodeError::UnknownKind(k) => write!(f, "unknown frame kind {k}"),
403 FrameDecodeError::UnknownFlags(b) => write!(f, "unknown frame flag bits {b:#04x}"),
404 FrameDecodeError::RunIdNotUtf8 => write!(f, "run_id is not valid UTF-8"),
405 FrameDecodeError::EmptyFrameWithPayload => {
406 write!(f, "an Empty frame must carry no payload")
407 }
408 FrameDecodeError::Payload(e) => write!(f, "invalid frame payload container: {e}"),
409 }
410 }
411}
412
413impl std::error::Error for FrameDecodeError {}
414
415const FLAG_RUN_ID: u8 = 1 << 0;
416const FLAG_RUN_TOTALS: u8 = 1 << 1;
417const KNOWN_FLAGS: u8 = FLAG_RUN_ID | FLAG_RUN_TOTALS;
418
419impl FrameHeader {
420 pub fn empty() -> Self {
423 FrameHeader {
424 kind: FrameKind::Empty,
425 committed_at: 0,
426 run_id: None,
427 run_totals: None,
428 }
429 }
430
431 fn header_len(&self) -> usize {
433 11 + self.run_id.as_ref().map_or(0, |id| 1 + id.len())
434 + if self.run_totals.is_some() { 16 } else { 0 }
435 }
436
437 fn encode_header_into(&self, version: u8, out: &mut Vec<u8>) -> Result<(), FrameEncodeError> {
439 if let Some(id) = &self.run_id {
440 if id.len() > u8::MAX as usize {
441 return Err(FrameEncodeError::RunIdTooLong(id.len()));
442 }
443 }
444 out.push(version);
445 out.push(self.kind as u8);
446 let mut flags = 0u8;
447 if self.run_id.is_some() {
448 flags |= FLAG_RUN_ID;
449 }
450 if self.run_totals.is_some() {
451 flags |= FLAG_RUN_TOTALS;
452 }
453 out.push(flags);
454 out.extend_from_slice(&self.committed_at.to_le_bytes());
455 if let Some(id) = &self.run_id {
456 out.push(id.len() as u8);
457 out.extend_from_slice(id.as_bytes());
458 }
459 if let Some(t) = &self.run_totals {
460 out.extend_from_slice(&t.run_rows.to_le_bytes());
461 out.extend_from_slice(&t.rows_cum.to_le_bytes());
462 }
463 Ok(())
464 }
465
466 pub fn encode(&self, payload: &[u8]) -> Result<Vec<u8>, FrameEncodeError> {
470 if self.kind == FrameKind::Empty && !payload.is_empty() {
471 return Err(FrameEncodeError::EmptyFrameWithPayload);
472 }
473 let mut out = Vec::with_capacity(self.header_len() + payload.len());
474 self.encode_header_into(FRAME_FORMAT_V1, &mut out)?;
475 out.extend_from_slice(payload);
476 Ok(out)
477 }
478
479 pub fn encode_frame(&self, chunks: &[&[u8]]) -> Result<Vec<u8>, FrameEncodeError> {
485 if self.kind == FrameKind::Empty {
486 if !chunks.is_empty() {
487 return Err(FrameEncodeError::EmptyFrameWithPayload);
488 }
489 let mut out = Vec::with_capacity(self.header_len());
490 self.encode_header_into(FRAME_FORMAT_V2, &mut out)?;
491 return Ok(out);
492 }
493 let container_len = payload_container_len(chunks).map_err(FrameEncodeError::Payload)?;
494 let mut out = Vec::with_capacity(self.header_len() + container_len);
495 self.encode_header_into(FRAME_FORMAT_V2, &mut out)?;
496 encode_payload_chunks_into(chunks, &mut out).map_err(FrameEncodeError::Payload)?;
497 Ok(out)
498 }
499
500 pub fn decode_chunks(bytes: &[u8]) -> Result<(FrameHeader, Vec<&[u8]>), FrameDecodeError> {
505 let (&version, _) = bytes.split_first().ok_or(FrameDecodeError::Truncated)?;
506 let (header, payload) = Self::decode(bytes)?;
507 let chunks = if header.kind == FrameKind::Empty {
508 Vec::new()
509 } else if version == FRAME_FORMAT_V1 {
510 vec![payload]
511 } else {
512 decode_payload_chunks(payload).map_err(FrameDecodeError::Payload)?
513 };
514 Ok((header, chunks))
515 }
516
517 pub fn decode(bytes: &[u8]) -> Result<(FrameHeader, &[u8]), FrameDecodeError> {
522 let (&version, rest) = bytes.split_first().ok_or(FrameDecodeError::Truncated)?;
523 if version != FRAME_FORMAT_V1 && version != FRAME_FORMAT_V2 {
524 return Err(FrameDecodeError::UnknownFormat(version));
525 }
526 let (&kind_byte, rest) = rest.split_first().ok_or(FrameDecodeError::Truncated)?;
527 let kind = match kind_byte {
528 0 => FrameKind::Rows,
529 1 => FrameKind::Ddl,
530 2 => FrameKind::Empty,
531 other => return Err(FrameDecodeError::UnknownKind(other)),
532 };
533 let (&flags, rest) = rest.split_first().ok_or(FrameDecodeError::Truncated)?;
534 if flags & !KNOWN_FLAGS != 0 {
535 return Err(FrameDecodeError::UnknownFlags(flags & !KNOWN_FLAGS));
536 }
537 if rest.len() < 8 {
538 return Err(FrameDecodeError::Truncated);
539 }
540 let (ts, mut rest) = rest.split_at(8);
541 let committed_at = i64::from_le_bytes(ts.try_into().expect("8-byte split"));
542 let run_id = if flags & FLAG_RUN_ID != 0 {
543 let (&len, tail) = rest.split_first().ok_or(FrameDecodeError::Truncated)?;
544 if tail.len() < len as usize {
545 return Err(FrameDecodeError::Truncated);
546 }
547 let (id, tail) = tail.split_at(len as usize);
548 rest = tail;
549 Some(
550 std::str::from_utf8(id)
551 .map_err(|_| FrameDecodeError::RunIdNotUtf8)?
552 .to_owned(),
553 )
554 } else {
555 None
556 };
557 let run_totals = if flags & FLAG_RUN_TOTALS != 0 {
558 if rest.len() < 16 {
559 return Err(FrameDecodeError::Truncated);
560 }
561 let (raw, tail) = rest.split_at(16);
562 rest = tail;
563 Some(RunTotals {
564 run_rows: u64::from_le_bytes(raw[..8].try_into().expect("8-byte split")),
565 rows_cum: u64::from_le_bytes(raw[8..].try_into().expect("8-byte split")),
566 })
567 } else {
568 None
569 };
570 if kind == FrameKind::Empty && !rest.is_empty() {
571 return Err(FrameDecodeError::EmptyFrameWithPayload);
572 }
573 Ok((
574 FrameHeader {
575 kind,
576 committed_at,
577 run_id,
578 run_totals,
579 },
580 rest,
581 ))
582 }
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 fn full_header() -> FrameHeader {
590 FrameHeader {
591 kind: FrameKind::Rows,
592 committed_at: 1_752_000_000_123,
593 run_id: Some("w:0000000000000abc".to_owned()),
594 run_totals: Some(RunTotals {
595 run_rows: 42,
596 rows_cum: 9_000_000_001,
597 }),
598 }
599 }
600
601 #[test]
602 fn round_trips_every_field_combination() {
603 let payloads: &[&[u8]] = &[b"", b"[{\"op\":\"add\"}]"];
604 for kind in [FrameKind::Rows, FrameKind::Ddl] {
605 for run_id in [None, Some("r-1".to_owned())] {
606 for run_totals in [
607 None,
608 Some(RunTotals {
609 run_rows: 7,
610 rows_cum: 100,
611 }),
612 ] {
613 for payload in payloads {
614 let header = FrameHeader {
615 kind,
616 committed_at: -5, run_id: run_id.clone(),
618 run_totals,
619 };
620 let bytes = header.encode(payload).unwrap();
621 let (back, tail) = FrameHeader::decode(&bytes).unwrap();
622 assert_eq!(back, header);
623 assert_eq!(tail, *payload);
624 }
625 }
626 }
627 }
628 }
629
630 #[test]
631 fn empty_frame_round_trips_and_rejects_payload() {
632 let bytes = FrameHeader::empty().encode(b"").unwrap();
633 let (back, tail) = FrameHeader::decode(&bytes).unwrap();
634 assert_eq!(back, FrameHeader::empty());
635 assert!(tail.is_empty());
636
637 assert_eq!(
638 FrameHeader::empty().encode(b"x"),
639 Err(FrameEncodeError::EmptyFrameWithPayload)
640 );
641 let mut forged = bytes;
643 forged.push(0xff);
644 assert_eq!(
645 FrameHeader::decode(&forged),
646 Err(FrameDecodeError::EmptyFrameWithPayload)
647 );
648 }
649
650 #[test]
651 fn truncation_at_every_boundary_is_an_error_not_a_panic() {
652 let bytes = full_header().encode(b"payload").unwrap();
653 let payload_start = bytes.len() - "payload".len();
654 for cut in 0..payload_start {
655 assert_eq!(
656 FrameHeader::decode(&bytes[..cut]),
657 Err(FrameDecodeError::Truncated),
658 "cut at {cut}"
659 );
660 }
661 assert!(FrameHeader::decode(&bytes[..payload_start]).is_ok());
663 }
664
665 #[test]
666 fn unknown_version_kind_and_flags_fail_closed() {
667 let bytes = full_header().encode(b"p").unwrap();
668
669 let mut v = bytes.clone();
670 v[0] = 3;
671 assert_eq!(
672 FrameHeader::decode(&v),
673 Err(FrameDecodeError::UnknownFormat(3))
674 );
675
676 let mut k = bytes.clone();
677 k[1] = 3; assert_eq!(
679 FrameHeader::decode(&k),
680 Err(FrameDecodeError::UnknownKind(3))
681 );
682
683 let mut f = bytes;
684 f[2] |= 1 << 5;
685 assert_eq!(
686 FrameHeader::decode(&f),
687 Err(FrameDecodeError::UnknownFlags(1 << 5))
688 );
689 }
690
691 #[test]
692 fn run_id_limits() {
693 let header = FrameHeader {
694 run_id: Some("x".repeat(256)),
695 ..full_header()
696 };
697 assert_eq!(header.encode(b""), Err(FrameEncodeError::RunIdTooLong(256)));
698
699 let max = FrameHeader {
700 run_id: Some("y".repeat(255)),
701 ..full_header()
702 };
703 let bytes = max.encode(b"tail").unwrap();
704 let (back, tail) = FrameHeader::decode(&bytes).unwrap();
705 assert_eq!(back.run_id.as_deref().map(str::len), Some(255));
706 assert_eq!(tail, b"tail");
707 }
708
709 #[test]
710 fn non_utf8_run_id_is_rejected() {
711 let mut bytes = FrameHeader {
712 run_id: Some("ab".to_owned()),
713 run_totals: None,
714 ..full_header()
715 }
716 .encode(b"")
717 .unwrap();
718 let n = bytes.len();
720 bytes[n - 2] = 0xff;
721 bytes[n - 1] = 0xfe;
722 assert_eq!(
723 FrameHeader::decode(&bytes),
724 Err(FrameDecodeError::RunIdNotUtf8)
725 );
726 }
727
728 #[test]
729 fn payload_chunks_round_trip_without_flattening_boundaries() {
730 let chunks: &[&[u8]] = &[b"[{\"n\":1}]", b"[]", b"[{\"n\":2},{\"n\":3}]"];
731 let encoded = encode_payload_chunks(chunks).unwrap();
732 assert_eq!(encoded[0], PAYLOAD_CONTAINER_SENTINEL);
733 assert_eq!(decode_payload_chunks(&encoded).unwrap(), chunks);
734 }
735
736 #[test]
737 fn v2_frames_round_trip_chunks_in_one_buffer() {
738 let header = full_header();
739 let chunks: &[&[u8]] = &[b"[{\"n\":1}]", b"[{\"n\":2},{\"n\":3}]"];
740 let bytes = header.encode_frame(chunks).unwrap();
741 assert_eq!(
742 bytes[0], FRAME_FORMAT_V2,
743 "container frames must be version-stamped so a pre-container reader errors with \
744 UnknownFormat, never a JSON parse failure misread as corruption"
745 );
746 let (back, decoded) = FrameHeader::decode_chunks(&bytes).unwrap();
747 assert_eq!(back, header);
748 assert_eq!(decoded, chunks);
749
750 let empty = FrameHeader::empty().encode_frame(&[]).unwrap();
752 assert_eq!(empty[0], FRAME_FORMAT_V2);
753 let (back, decoded) = FrameHeader::decode_chunks(&empty).unwrap();
754 assert_eq!(back, FrameHeader::empty());
755 assert!(decoded.is_empty());
756 }
757
758 #[test]
759 fn v1_legacy_frames_decode_as_one_chunk_forever() {
760 let legacy = b"[{\"op\":\"add\"}]";
763 let bytes = full_header().encode(legacy).unwrap();
764 assert_eq!(bytes[0], FRAME_FORMAT_V1);
765 let (_, chunks) = FrameHeader::decode_chunks(&bytes).unwrap();
766 assert_eq!(chunks, vec![legacy.as_slice()]);
767 }
768
769 #[test]
770 fn encode_frame_rejects_producer_misuse() {
771 assert_eq!(
772 full_header().encode_frame(&[]),
773 Err(FrameEncodeError::Payload(PayloadEncodeError::Empty))
774 );
775 assert_eq!(
776 full_header().encode_frame(&[b"ok", b""]),
777 Err(FrameEncodeError::Payload(PayloadEncodeError::EmptyChunk))
778 );
779 assert_eq!(
780 FrameHeader::empty().encode_frame(&[b"x"]),
781 Err(FrameEncodeError::EmptyFrameWithPayload)
782 );
783 }
784
785 #[test]
786 fn v2_frame_with_a_non_container_payload_fails_closed() {
787 let mut bytes = full_header().encode_frame(&[b"[{\"n\":1}]"]).unwrap();
790 let payload_at = bytes.len() - (PAYLOAD_CONTAINER_HEADER_LEN + 4 + 9);
791 assert_eq!(bytes[payload_at], PAYLOAD_CONTAINER_SENTINEL);
792 bytes[payload_at] ^= 1;
793 assert_eq!(
794 FrameHeader::decode_chunks(&bytes),
795 Err(FrameDecodeError::Payload(
796 PayloadDecodeError::MissingSentinel
797 ))
798 );
799 }
800
801 #[test]
802 fn hostile_chunk_counts_cannot_amplify_allocation() {
803 let mut crafted = Vec::new();
807 crafted.push(PAYLOAD_CONTAINER_SENTINEL);
808 crafted.extend_from_slice(PAYLOAD_CONTAINER_MAGIC);
809 crafted.push(PAYLOAD_CONTAINER_V1);
810 crafted.extend_from_slice(&1024u32.to_le_bytes());
811 crafted.extend_from_slice(&vec![0u8; 4 * 1024]);
812 assert_eq!(
813 decode_payload_chunks(&crafted),
814 Err(PayloadDecodeError::EmptyChunk)
815 );
816
817 let mut over_cap = Vec::new();
819 over_cap.push(PAYLOAD_CONTAINER_SENTINEL);
820 over_cap.extend_from_slice(PAYLOAD_CONTAINER_MAGIC);
821 over_cap.push(PAYLOAD_CONTAINER_V1);
822 over_cap.extend_from_slice(&((MAX_CONTAINER_CHUNKS as u32) + 1).to_le_bytes());
823 over_cap.extend_from_slice(&vec![1u8; 5 * (MAX_CONTAINER_CHUNKS + 1)]);
824 assert_eq!(
825 decode_payload_chunks(&over_cap),
826 Err(PayloadDecodeError::TooManyChunks(MAX_CONTAINER_CHUNKS + 1))
827 );
828 }
829
830 #[test]
831 fn malformed_payload_containers_fail_closed() {
832 assert_eq!(encode_payload_chunks(&[]), Err(PayloadEncodeError::Empty));
833 assert_eq!(
834 encode_payload_chunks(&[b"one", b""]),
835 Err(PayloadEncodeError::EmptyChunk)
836 );
837
838 assert_eq!(
839 decode_payload_chunks(b"[{\"op\":\"add\"}]"),
840 Err(PayloadDecodeError::MissingSentinel),
841 "a non-container payload is an error here — legacy handling is the FRAME version's \
842 job (decode_chunks), never a byte-sniffing fallback"
843 );
844
845 let valid = encode_payload_chunks(&[b"one", b"two"]).unwrap();
846 for cut in 1..valid.len() {
847 let decoded = decode_payload_chunks(&valid[..cut]);
848 assert!(decoded.is_err(), "cut at {cut} unexpectedly decoded");
849 }
850
851 let mut bad_magic = valid.clone();
852 bad_magic[1] ^= 1;
853 assert_eq!(
854 decode_payload_chunks(&bad_magic),
855 Err(PayloadDecodeError::InvalidMagic)
856 );
857
858 let mut future = valid.clone();
859 future[1 + PAYLOAD_CONTAINER_MAGIC.len()] = 2;
860 assert_eq!(
861 decode_payload_chunks(&future),
862 Err(PayloadDecodeError::UnknownVersion(2))
863 );
864
865 let mut empty = valid.clone();
866 let count_at = 1 + PAYLOAD_CONTAINER_MAGIC.len() + 1;
867 empty[count_at..count_at + 4].copy_from_slice(&0u32.to_le_bytes());
868 assert_eq!(
869 decode_payload_chunks(&empty),
870 Err(PayloadDecodeError::Empty)
871 );
872
873 let mut impossible_count = valid.clone();
874 impossible_count[count_at..count_at + 4].copy_from_slice(&u32::MAX.to_le_bytes());
875 assert_eq!(
876 decode_payload_chunks(&impossible_count),
877 Err(PayloadDecodeError::TooManyChunks(u32::MAX as usize))
878 );
879
880 let mut trailing = valid;
881 trailing.push(0);
882 assert_eq!(
883 decode_payload_chunks(&trailing),
884 Err(PayloadDecodeError::TrailingBytes(1))
885 );
886 }
887
888 #[test]
889 fn payload_length_step_pins_validation_and_overflow_boundaries() {
890 assert_eq!(
891 payload_container_len_step(PAYLOAD_CONTAINER_HEADER_LEN, 1),
892 Ok(PAYLOAD_CONTAINER_HEADER_LEN + 5)
893 );
894 assert_eq!(
895 payload_container_len_step(PAYLOAD_CONTAINER_HEADER_LEN, 0),
896 Err(PayloadEncodeError::EmptyChunk)
897 );
898 assert_eq!(
899 payload_container_len_step(usize::MAX, 1),
900 Err(PayloadEncodeError::ContainerTooLarge)
901 );
902 #[cfg(target_pointer_width = "64")]
903 assert_eq!(
904 payload_container_len_step(PAYLOAD_CONTAINER_HEADER_LEN, u32::MAX as usize + 1),
905 Err(PayloadEncodeError::ChunkTooLarge(u32::MAX as usize + 1))
906 );
907 }
908}
909
910#[cfg(kani)]
911mod kani_proofs {
912 use super::*;
913
914 #[kani::proof]
917 fn proof_payload_container_len_step_is_exact_complete() {
918 let current: usize = kani::any();
919 let chunk_len: usize = kani::any();
920 let actual = payload_container_len_step(current, chunk_len);
921
922 if chunk_len == 0 {
923 assert_eq!(actual, Err(PayloadEncodeError::EmptyChunk));
924 } else if chunk_len > u32::MAX as usize {
925 assert_eq!(actual, Err(PayloadEncodeError::ChunkTooLarge(chunk_len)));
926 } else if chunk_len > usize::MAX - 4 || current > usize::MAX - 4 - chunk_len {
927 assert_eq!(actual, Err(PayloadEncodeError::ContainerTooLarge));
928 } else {
929 assert_eq!(actual, Ok(current + 4 + chunk_len));
930 }
931 }
932
933 #[kani::proof]
937 #[kani::unwind(14)]
938 fn proof_frame_header_decode_never_panics_up_to_12_bytes_bounded() {
939 let bytes: [u8; 12] = kani::any();
940 let len: u8 = kani::any();
941 kani::assume(len <= bytes.len() as u8);
942
943 let decoded = FrameHeader::decode(&bytes[..len as usize]);
944 kani::cover!(decoded.is_ok());
945 kani::cover!(matches!(decoded, Err(FrameDecodeError::Truncated)));
946 kani::cover!(matches!(decoded, Err(FrameDecodeError::UnknownFormat(_))));
947 kani::cover!(matches!(decoded, Err(FrameDecodeError::UnknownKind(_))));
948 kani::cover!(matches!(decoded, Err(FrameDecodeError::UnknownFlags(_))));
949 kani::cover!(matches!(
950 decoded,
951 Err(FrameDecodeError::EmptyFrameWithPayload)
952 ));
953 }
954
955 #[kani::proof]
958 #[kani::unwind(15)]
959 fn proof_frame_header_decode_one_byte_run_id_is_total_bounded() {
960 let committed_at: i64 = kani::any();
961 let id_byte: u8 = kani::any();
962 let mut bytes = [0u8; 13];
963 bytes[0] = FRAME_FORMAT_V1;
964 bytes[1] = FrameKind::Rows as u8;
965 bytes[2] = FLAG_RUN_ID;
966 bytes[3..11].copy_from_slice(&committed_at.to_le_bytes());
967 bytes[11] = 1;
968 bytes[12] = id_byte;
969
970 let decoded = FrameHeader::decode(&bytes);
971 if id_byte.is_ascii() {
972 let (header, payload) = decoded.unwrap();
973 assert_eq!(header.kind, FrameKind::Rows);
974 assert_eq!(header.committed_at, committed_at);
975 assert_eq!(header.run_id.unwrap().as_bytes(), &[id_byte]);
976 assert!(payload.is_empty());
977 } else {
978 assert_eq!(decoded, Err(FrameDecodeError::RunIdNotUtf8));
979 }
980 }
981
982 #[kani::proof]
987 #[kani::unwind(10)]
988 fn proof_decode_payload_chunks_never_panics_up_to_18_bytes_bounded() {
989 let bytes: [u8; 18] = kani::any();
990 let len: u8 = kani::any();
991 kani::assume(len <= bytes.len() as u8);
992
993 let _ = decode_payload_chunks(&bytes[..len as usize]);
994 }
995
996 fn assert_payload_container_one_chunk_layout_and_round_trip(payload: &[u8]) {
997 let chunks = [payload];
998 let encoded = encode_payload_chunks(&chunks).unwrap();
999
1000 assert_eq!(
1001 encoded.len(),
1002 PAYLOAD_CONTAINER_HEADER_LEN + 4 + payload.len()
1003 );
1004 assert_eq!(encoded[0], 0x89);
1005 assert_eq!(&encoded[1..5], b"RJP\0");
1006 assert_eq!(encoded[5], 1);
1007 assert_eq!(&encoded[6..10], &1u32.to_le_bytes());
1008 assert_eq!(&encoded[10..14], &(payload.len() as u32).to_le_bytes());
1009 assert_eq!(&encoded[14..], payload);
1010 assert_eq!(decode_payload_chunks(&encoded).unwrap(), chunks);
1011 }
1012
1013 #[kani::proof]
1016 #[kani::unwind(18)]
1017 fn proof_payload_container_one_byte_chunk_layout_and_round_trip_bounded() {
1018 let payload: [u8; 1] = kani::any();
1019 assert_payload_container_one_chunk_layout_and_round_trip(&payload);
1020 }
1021
1022 #[kani::proof]
1025 #[kani::unwind(18)]
1026 fn proof_payload_container_two_byte_chunk_layout_and_round_trip_bounded() {
1027 let payload: [u8; 2] = kani::any();
1028 assert_payload_container_one_chunk_layout_and_round_trip(&payload);
1029 }
1030
1031 #[kani::proof]
1034 #[kani::unwind(24)]
1035 fn proof_payload_container_two_chunk_layout_and_round_trip_bounded() {
1036 let first: [u8; 1] = kani::any();
1037 let second: [u8; 2] = kani::any();
1038 let first_len = first.len();
1039 let second_len = second.len();
1040 let chunks: [&[u8]; 2] = [&first, &second];
1041 let encoded = encode_payload_chunks(&chunks).unwrap();
1042 let second_prefix = 14 + first_len;
1043 let second_payload = second_prefix + 4;
1044
1045 assert_eq!(
1046 encoded.len(),
1047 PAYLOAD_CONTAINER_HEADER_LEN + 8 + first_len + second_len
1048 );
1049 assert_eq!(&encoded[6..10], &2u32.to_le_bytes());
1050 assert_eq!(&encoded[10..14], &(first_len as u32).to_le_bytes());
1051 assert_eq!(&encoded[14..second_prefix], &first);
1052 assert_eq!(
1053 &encoded[second_prefix..second_payload],
1054 &(second_len as u32).to_le_bytes()
1055 );
1056 assert_eq!(&encoded[second_payload..], &second);
1057 assert_eq!(decode_payload_chunks(&encoded).unwrap(), chunks);
1058 }
1059
1060 fn assert_v1_frame_no_options_layout_and_round_trip(kind: FrameKind) {
1061 let header = FrameHeader {
1062 kind,
1063 committed_at: 0,
1064 run_id: None,
1065 run_totals: None,
1066 };
1067 let encoded = header.encode(b"x").unwrap();
1068
1069 assert_eq!(encoded.len(), 12);
1070 assert_eq!(encoded[0], 1);
1071 assert_eq!(encoded[1], header.kind as u8);
1072 assert_eq!(encoded[2], 0);
1073 assert_eq!(&encoded[3..11], &header.committed_at.to_le_bytes());
1074 assert_eq!(&encoded[11..], b"x");
1075 assert_eq!(FrameHeader::decode(&encoded), Ok((header, b"x".as_slice())));
1076 }
1077
1078 #[kani::proof]
1081 #[kani::unwind(18)]
1082 fn proof_v1_rows_frame_no_options_layout_and_round_trip_bounded() {
1083 assert_v1_frame_no_options_layout_and_round_trip(FrameKind::Rows);
1084 }
1085
1086 #[kani::proof]
1089 #[kani::unwind(18)]
1090 fn proof_v1_ddl_frame_no_options_layout_and_round_trip_bounded() {
1091 assert_v1_frame_no_options_layout_and_round_trip(FrameKind::Ddl);
1092 }
1093
1094 #[kani::proof]
1097 #[kani::unwind(30)]
1098 fn proof_v1_frame_totals_layout_and_round_trip_complete() {
1099 let header = FrameHeader {
1100 kind: FrameKind::Rows,
1101 committed_at: kani::any(),
1102 run_id: None,
1103 run_totals: Some(RunTotals {
1104 run_rows: kani::any(),
1105 rows_cum: kani::any(),
1106 }),
1107 };
1108 let encoded = header.encode(b"").unwrap();
1109 let totals = header.run_totals.unwrap();
1110
1111 assert_eq!(encoded.len(), 27);
1112 assert_eq!(encoded[0], 1);
1113 assert_eq!(encoded[1], header.kind as u8);
1114 assert_eq!(encoded[2], FLAG_RUN_TOTALS);
1115 assert_eq!(&encoded[3..11], &header.committed_at.to_le_bytes());
1116 assert_eq!(&encoded[11..19], &totals.run_rows.to_le_bytes());
1117 assert_eq!(&encoded[19..27], &totals.rows_cum.to_le_bytes());
1118 assert_eq!(FrameHeader::decode(&encoded), Ok((header, b"".as_slice())));
1119 }
1120
1121 #[kani::proof]
1125 #[kani::unwind(38)]
1126 fn proof_v1_frame_both_options_layout_bounded() {
1127 let header = FrameHeader {
1128 kind: FrameKind::Ddl,
1129 committed_at: -5,
1130 run_id: Some("r-1".to_owned()),
1131 run_totals: Some(RunTotals {
1132 run_rows: 7,
1133 rows_cum: 100,
1134 }),
1135 };
1136 let encoded = header.encode(b"x").unwrap();
1137
1138 assert_eq!(encoded.len(), 32);
1139 assert_eq!(encoded[0], 1);
1140 assert_eq!(encoded[1], FrameKind::Ddl as u8);
1141 assert_eq!(encoded[2], FLAG_RUN_ID | FLAG_RUN_TOTALS);
1142 assert_eq!(&encoded[3..11], &(-5i64).to_le_bytes());
1143 assert_eq!(encoded[11], 3);
1144 assert_eq!(&encoded[12..15], b"r-1");
1145 assert_eq!(&encoded[15..23], &7u64.to_le_bytes());
1146 assert_eq!(&encoded[23..31], &100u64.to_le_bytes());
1147 assert_eq!(&encoded[31..], b"x");
1148 }
1149
1150 #[kani::proof]
1154 #[kani::unwind(28)]
1155 fn proof_v2_frame_one_chunk_round_trip_bounded() {
1156 let header = FrameHeader {
1157 kind: FrameKind::Rows,
1158 committed_at: -1,
1159 run_id: None,
1160 run_totals: None,
1161 };
1162 let chunks: [&[u8]; 1] = [b"x"];
1163 let encoded = header.encode_frame(&chunks).unwrap();
1164
1165 assert_eq!(encoded[0], 2);
1166 assert_eq!(encoded[11], 0x89);
1167 assert_eq!(
1168 FrameHeader::decode_chunks(&encoded),
1169 Ok((header, chunks.to_vec()))
1170 );
1171 }
1172}