#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)] //! Length-prefixed binary framing and JSON (de)serialization helpers for //! the IPC wire protocol. //! //! Flow: `write_frame`/`read_frame` handle the raw byte-level framing //! (4-byte big-endian length header + payload) over any `Read`/`Write`; //! `serialize_frame`/`deserialize_frame` handle the JSON layer on top. //! `Connection` (see `conn.rs`) composes both layers for a full send/receive. //! //! Why: a fixed-size length prefix lets the reader know exactly how many //! bytes to pull before attempting to parse, avoiding partial-JSON reads //! over a stream socket. use std::io::{Read, Write}; use anyhow::Result; /// Upper bound on a single frame's byte size (64 MiB), enforced on both /// the write and read paths to bound memory use and reject malformed or /// malicious oversized length headers. pub(crate) const MAX_FRAME_SIZE: usize = 64 * 1024 * 1024; /// Write `data` as a length-prefixed frame: 4-byte big-endian length /// followed by the raw bytes, then flush. /// /// Why: rejects frames over `MAX_FRAME_SIZE` to bound memory use on the /// reading side before any bytes are read. pub fn write_frame(writer: &mut W, data: &[u8]) -> Result<()> { let len = data.len(); if len > MAX_FRAME_SIZE { anyhow::bail!("frame too large: {len} bytes exceeds 64 MiB limit"); } let len_bytes = (len as u32).to_be_bytes(); writer.write_all(&len_bytes)?; writer.write_all(data)?; writer.flush()?; Ok(()) } /// Read one length-prefixed frame written by `write_frame`. /// /// Flow: read 4-byte length header → on clean EOF before any bytes, /// return `Ok(None)` (peer closed) → validate against `MAX_FRAME_SIZE` /// → read the payload. /// /// Return: `Ok(None)` signals a graceful connection close, distinct /// from an `Err` mid-frame I/O failure. pub fn read_frame(reader: &mut R) -> Result>> { let mut len_buf = [0u8; 4]; match reader.read_exact(&mut len_buf) { Ok(()) => {} Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None), Err(e) => return Err(e.into()), } let len = u32::from_be_bytes(len_buf) as usize; if len > MAX_FRAME_SIZE { anyhow::bail!("frame too large: {len} bytes exceeds 64 MiB limit"); } let mut buf = vec![0u8; len]; reader.read_exact(&mut buf)?; Ok(Some(buf)) } /// Serialize `value` to JSON bytes, rejecting output over `MAX_FRAME_SIZE`. pub fn serialize_frame(value: &T) -> Result> { let json = serde_json::to_vec(value)?; if json.len() > MAX_FRAME_SIZE { anyhow::bail!("serialized frame too large: {} bytes", json.len()); } Ok(json) } /// Deserialize a frame's raw JSON bytes into `T`. pub fn deserialize_frame<'a, T: serde::Deserialize<'a>>(data: &'a [u8]) -> Result { Ok(serde_json::from_slice(data)?) } #[cfg(test)] mod tests { use super::*; /// Write a value, read it back, and verify exact equality. fn roundtrip_bytes(data: &[u8]) { let mut buf: Vec = Vec::new(); write_frame(&mut buf, data).unwrap(); let read_back = read_frame(&mut buf.as_slice()) .unwrap() .expect("expected Some(frame)"); assert_eq!(read_back, data); } #[test] fn test_write_read_roundtrip_empty() { roundtrip_bytes(b""); } #[test] fn test_write_read_roundtrip_small_text() { roundtrip_bytes(b"hello world"); } #[test] fn test_write_read_roundtrip_binary() { roundtrip_bytes(&[0x00, 0xFF, 0xAB, 0xCD, 0x01, 0x02, 0x03]); } #[test] fn test_write_read_roundtrip_large() { let data = vec![0x42u8; 100_000]; roundtrip_bytes(&data); } #[test] fn test_write_rejects_too_large_frame() { let oversized = vec![0u8; MAX_FRAME_SIZE + 1]; let mut buf = Vec::new(); let result = write_frame(&mut buf, &oversized); assert!(result.is_err()); let err = result.unwrap_err().to_string(); assert!(err.contains("too large") || err.contains("64 MiB")); } #[test] fn test_read_rejects_too_large_header() { // Manually craft a 4-byte length header that exceeds MAX_FRAME_SIZE let len = (MAX_FRAME_SIZE as u32).wrapping_add(1); let header = len.to_be_bytes(); let mut buf = Vec::from(&header[..]); buf.extend_from_slice(b"dummy"); let result = read_frame(&mut buf.as_slice()); assert!(result.is_err()); let err = result.unwrap_err().to_string(); assert!(err.contains("too large")); } #[test] fn test_read_empty_buf_returns_none() { let empty: &[u8] = &[]; let result = read_frame(&mut &empty[..]).unwrap(); assert!(result.is_none(), "expected None for empty reader"); } #[test] fn test_read_partial_header_returns_none() { // Only 2 bytes of the 4-byte header → EOF let partial: &[u8] = &[0x00, 0x01]; let result = read_frame(&mut &partial[..]).unwrap(); assert!(result.is_none(), "expected None for partial header"); } #[test] fn test_read_truncated_payload_returns_err() { let mut buf = Vec::new(); let header = (10u32).to_be_bytes(); buf.extend_from_slice(&header); buf.extend_from_slice(b"abc"); // only 3 of 10 bytes let result = read_frame(&mut buf.as_slice()); assert!(result.is_err(), "truncated payload should error"); } #[test] fn test_serialize_deserialize_roundtrip() { use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] struct Msg { id: u32, content: String, tags: Vec, } let original = Msg { id: 42, content: "hello world".into(), tags: vec!["foo".into(), "bar".into()], }; let bytes = serialize_frame(&original).unwrap(); let deserialized: Msg = deserialize_frame(&bytes).unwrap(); assert_eq!(original, deserialized); } #[test] fn test_serialize_rejects_oversized_value() { let huge = vec![0u8; MAX_FRAME_SIZE + 1]; let result = serialize_frame(&huge); assert!(result.is_err()); } #[test] fn test_deserialize_malformed_json_errors() { let bad_json = b"this is not json"; let result: Result = deserialize_frame(bad_json); assert!(result.is_err()); } }