diff --git a/crates/ferese-ipc/src/lib.rs b/crates/ferese-ipc/src/lib.rs index b3fb71b..ad829df 100644 --- a/crates/ferese-ipc/src/lib.rs +++ b/crates/ferese-ipc/src/lib.rs @@ -121,17 +121,36 @@ pub fn write_frame(writer: &mut impl Write, value: &T) -> Result<( #[cfg(test)] mod tests { + use std::io::Cursor; + use super::*; + struct ChunkedReader { + inner: R, + chunk_size: usize, + } + + impl Read for ChunkedReader { + fn read(&mut self, buffer: &mut [u8]) -> io::Result { + let limit = buffer.len().min(self.chunk_size); + self.inner.read(&mut buffer[..limit]) + } + } + + fn request(id: u64, command: &str) -> Request { + Request { + version: VERSION, + id, + kind: "command".to_owned(), + command: command.to_owned(), + args: serde_json::json!({}), + } + } + #[test] fn framed_json_round_trips() { - let request = Request { - version: VERSION, - id: 42, - kind: "command".to_owned(), - command: "focus".to_owned(), - args: serde_json::json!({ "direction": "left" }), - }; + let mut request = request(42, "focus"); + request.args = serde_json::json!({ "direction": "left" }); let mut bytes = Vec::new(); write_frame(&mut bytes, &request).unwrap(); let decoded: Request = read_frame(&mut bytes.as_slice()).unwrap(); @@ -142,6 +161,41 @@ mod tests { assert_eq!(decoded.args["direction"], "left"); } + #[test] + fn reads_fragmented_frames_and_preserves_frame_boundaries() { + let mut bytes = Vec::new(); + write_frame(&mut bytes, &request(1, "get-workspaces")).unwrap(); + write_frame(&mut bytes, &request(2, "get-outputs")).unwrap(); + let mut reader = ChunkedReader { + inner: Cursor::new(bytes), + chunk_size: 3, + }; + + let first: Request = read_frame(&mut reader).unwrap(); + let second: Request = read_frame(&mut reader).unwrap(); + + assert_eq!((first.id, first.command.as_str()), (1, "get-workspaces")); + assert_eq!((second.id, second.command.as_str()), (2, "get-outputs")); + } + + #[test] + fn rejects_invalid_and_truncated_payloads_without_panicking() { + let invalid = b"not-json"; + let mut invalid_frame = Vec::from((invalid.len() as u32).to_be_bytes()); + invalid_frame.extend_from_slice(invalid); + assert!(matches!( + read_frame::(&mut invalid_frame.as_slice()), + Err(FrameError::InvalidJson(_)) + )); + + let mut truncated = Vec::from(32_u32.to_be_bytes()); + truncated.extend_from_slice(b"short"); + assert!(matches!( + read_frame::(&mut truncated.as_slice()), + Err(FrameError::Io(error)) if error.kind() == io::ErrorKind::UnexpectedEof + )); + } + #[test] fn rejects_oversized_frames_before_allocating_payload() { let size = (MAX_PAYLOAD_SIZE as u32 + 1).to_be_bytes();