//! Invariants for the `EventEnvelope` length-prefixed JSON framing //! (ADR-014): no-panic, allocation bounded by `MAX_FRAME_SIZE`, clean //! `FrameError` classes on truncation/oversize/zero-length, consumption //! accounting, and structural round-trip via `write_frame`. use std::io::Cursor; use alkcall::protocol::wire::{EventEnvelope, FrameError, FrameFramedReader, FrameFramedWriter}; pub const MAX_FRAME_SIZE: u32 = 64 * 1024 * 1024; pub fn fuzz_envelope_frame(data: &[u8]) { let result = read_single(data); if let Ok(envelope) = &result { assert_envelope_shape(envelope); assert_write_round_trip(envelope); } if data.len() >= 8 { let len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize; if len > 0 && 4 + len <= data.len().min(MAX_FRAME_SIZE as usize + 4) && len < MAX_FRAME_SIZE as usize && data[4 + len - 1] == b'}' { let mut stream = data[..4 + len].to_vec(); stream.push(0x7b); let claimed = (len as u32 + 1).to_be_bytes(); stream[..4].copy_from_slice(&claimed); match read_single(&stream) { Err(FrameError::Json(_)) => {} other => panic!( "a byte after a complete JSON body, counted in the length prefix, must surface as a Json error, got {other:?}" ), } } } } fn read_single(data: &[u8]) -> Result { let mut reader = FrameFramedReader::new(Cursor::new(data.to_vec())); let result = tokio_runtime().block_on(reader.read_frame()); match &result { Err(FrameError::ConnectionClosed) => { assert_truncation_shape(data); } Err(FrameError::InvalidFrame) => { assert_invalid_frame_shape(data); } Err(FrameError::Json(_)) => { assert_json_error_shape(data); } Err(FrameError::Io(e)) => { panic!("an in-memory cursor cannot produce io errors, got {e}") } Ok(_) => { assert_valid_frame_shape(data); } } result } fn body_len(data: &[u8]) -> Option { if data.len() < 4 { return None; } let len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize; Some(len) } fn assert_truncation_shape(data: &[u8]) { match body_len(data) { None => { assert!(data.len() < 4, "prefix shorter than 4 bytes"); } Some(len) => { assert!( len > 0 && len <= MAX_FRAME_SIZE as usize, "ConnectionClosed with len {len} would mean an oversize/zero length was not rejected first" ); assert!( data.len() < 4 + len, "a complete body must never yield ConnectionClosed" ); } } } fn assert_invalid_frame_shape(data: &[u8]) { let len = body_len(data).expect("InvalidFrame requires a 4-byte prefix"); assert!( len == 0 || len > MAX_FRAME_SIZE as usize, "InvalidFrame with len {len} must not happen: 0 < len <= MAX is a valid length" ); } fn assert_json_error_shape(data: &[u8]) { let len = body_len(data).expect("Json errors require a 4-byte prefix"); assert!(len > 0 && len <= MAX_FRAME_SIZE as usize); assert!( 4 + len <= data.len(), "a Json error requires the full body to be present" ); } fn assert_valid_frame_shape(data: &[u8]) { let len = body_len(data).expect("a valid frame requires a 4-byte prefix"); assert!(len > 0 && len <= MAX_FRAME_SIZE as usize); assert!( data.len() >= 4 + len, "decoding a valid frame requires the full body to be present" ); } fn assert_envelope_shape(envelope: &EventEnvelope) { let json = serde_json::to_value(envelope).expect("envelope serializes"); let obj = json.as_object().expect("envelope serializes to an object"); assert_eq!( obj.get("type").and_then(|v| v.as_str()), Some(envelope.r#type.as_str()), "type field round-trips through serde" ); assert_eq!( obj.get("id").and_then(|v| v.as_str()), Some(envelope.id.as_str()) ); assert_eq!(obj.get("payload"), Some(&envelope.payload)); } fn assert_write_round_trip(envelope: &EventEnvelope) { let mut bytes = Vec::new(); { let mut writer = FrameFramedWriter::new(&mut bytes); tokio_runtime() .block_on(writer.write_frame(envelope)) .expect("in-memory writer cannot fail"); } let mut reader = FrameFramedReader::new(Cursor::new(bytes)); let decoded = tokio_runtime() .block_on(reader.read_frame()) .expect("a written frame re-decodes"); assert_eq!( &decoded, envelope, "structural round-trip (serde_json key order is not preserved, compare structurally)" ); } fn tokio_runtime() -> tokio::runtime::Runtime { tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("current-thread runtime builds") } #[cfg(test)] mod corpus_replay { use super::*; use std::path::PathBuf; #[test] fn committed_corpus_replays_through_the_invariants() { let corpus = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../corpus/envelope_frame"); let mut count = 0usize; for entry in std::fs::read_dir(&corpus).expect("corpus directory is committed") { let path = entry.expect("corpus entry readable").path(); let data = std::fs::read(&path).expect("corpus entry readable"); fuzz_envelope_frame(&data); count += 1; } assert!( count >= 32, "committed seed corpus is present, found {count}" ); } }