From 1ba221b2fa51c333159c14915e9e98a079993faf Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 4 Jul 2025 02:06:27 +0100 Subject: [PATCH 01/13] Refactor LengthFormat helpers to remove duplication --- src/frame.rs | 135 ++++++++++++++++++++++++++------------------------- 1 file changed, 69 insertions(+), 66 deletions(-) diff --git a/src/frame.rs b/src/frame.rs index 65148f00..e2df0e6b 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -4,10 +4,75 @@ //! Implementations may use any framing strategy suitable for the //! underlying transport. -use std::io; +use std::{convert::TryInto, io}; use bytes::{Buf, BufMut, BytesMut}; +/// Converts a byte slice into a `u64` according to the given prefix size and +/// endianness. +/// +/// Returns an error if the size is unsupported. +fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> io::Result { + Ok(match (size, endianness) { + (1, _) => u64::from(u8::from_ne_bytes([bytes[0]])), + (2, Endianness::Big) => u64::from(u16::from_be_bytes(bytes[..2].try_into().unwrap())), + (2, Endianness::Little) => u64::from(u16::from_le_bytes(bytes[..2].try_into().unwrap())), + (4, Endianness::Big) => u64::from(u32::from_be_bytes(bytes[..4].try_into().unwrap())), + (4, Endianness::Little) => u64::from(u32::from_le_bytes(bytes[..4].try_into().unwrap())), + (8, Endianness::Big) => u64::from_be_bytes(bytes[..8].try_into().unwrap()), + (8, Endianness::Little) => u64::from_le_bytes(bytes[..8].try_into().unwrap()), + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "unsupported length prefix size", + )); + } + }) +} + +/// Encodes an integer into bytes according to the given prefix size and +/// endianness. +/// +/// Returns an error if the integer does not fit into the requested size. +fn u64_to_bytes(len: usize, size: usize, endianness: Endianness) -> io::Result> { + Ok(match (size, endianness) { + (1, _) => vec![ + u8::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))?, + ], + (2, Endianness::Big) => u16::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? + .to_be_bytes() + .to_vec(), + (2, Endianness::Little) => u16::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? + .to_le_bytes() + .to_vec(), + (4, Endianness::Big) => u32::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? + .to_be_bytes() + .to_vec(), + (4, Endianness::Little) => u32::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? + .to_le_bytes() + .to_vec(), + (8, Endianness::Big) => u64::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? + .to_be_bytes() + .to_vec(), + (8, Endianness::Little) => u64::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? + .to_le_bytes() + .to_vec(), + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "unsupported length prefix size", + )); + } + }) +} + /// Byte order used for encoding and decoding length prefixes. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Endianness { @@ -67,29 +132,7 @@ impl LengthFormat { /// Returns an error if the prefix size is unsupported or if the decoded length does not fit in /// a `usize`. fn read_len(&self, bytes: &[u8]) -> io::Result { - let len = match (self.bytes, self.endianness) { - (1, _) => u64::from(u8::from_ne_bytes([bytes[0]])), - (2, Endianness::Big) => u64::from(u16::from_be_bytes([bytes[0], bytes[1]])), - (2, Endianness::Little) => u64::from(u16::from_le_bytes([bytes[0], bytes[1]])), - (4, Endianness::Big) => { - u64::from(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) - } - (4, Endianness::Little) => { - u64::from(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) - } - (8, Endianness::Big) => u64::from_be_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], - ]), - (8, Endianness::Little) => u64::from_le_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], - ]), - _ => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "unsupported length prefix size", - )); - } - }; + let len = bytes_to_u64(bytes, self.bytes, self.endianness)?; usize::try_from(len).map_err(|_| io::Error::other("frame too large")) } @@ -106,48 +149,8 @@ impl LengthFormat { /// Returns an error if `len` exceeds the maximum value for the configured prefix size or if the /// prefix size is not supported. fn write_len(&self, len: usize, dst: &mut BytesMut) -> io::Result<()> { - match (self.bytes, self.endianness) { - (1, _) => dst.put_u8( - u8::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))?, - ), - (2, Endianness::Big) => dst.put_slice( - &u16::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? - .to_be_bytes(), - ), - (2, Endianness::Little) => dst.put_slice( - &u16::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? - .to_le_bytes(), - ), - (4, Endianness::Big) => dst.put_slice( - &u32::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? - .to_be_bytes(), - ), - (4, Endianness::Little) => dst.put_slice( - &u32::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? - .to_le_bytes(), - ), - (8, Endianness::Big) => dst.put_slice( - &u64::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? - .to_be_bytes(), - ), - (8, Endianness::Little) => dst.put_slice( - &u64::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? - .to_le_bytes(), - ), - _ => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "unsupported length prefix size", - )); - } - } + let prefix = u64_to_bytes(len, self.bytes, self.endianness)?; + dst.put_slice(&prefix); Ok(()) } } From c3f81937c043919173f6b43f32a86b573c1ba305 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 4 Jul 2025 02:28:25 +0100 Subject: [PATCH 02/13] Improve length helper safety and tests --- src/frame.rs | 215 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 165 insertions(+), 50 deletions(-) diff --git a/src/frame.rs b/src/frame.rs index e2df0e6b..73a03709 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -6,71 +6,126 @@ use std::{convert::TryInto, io}; +const ERR_UNSUPPORTED_PREFIX: &str = "unsupported length prefix size"; +const ERR_FRAME_TOO_LARGE: &str = "frame too large"; + use bytes::{Buf, BufMut, BytesMut}; -/// Converts a byte slice into a `u64` according to the given prefix size and -/// endianness. +/// Converts a byte slice into a `u64` according to `size` and `endianness`. +/// +/// Only prefix sizes of `1`, `2`, `4`, or `8` bytes are supported. `bytes` must +/// contain at least `size` bytes. /// -/// Returns an error if the size is unsupported. -fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> io::Result { +/// # Errors +/// Returns [`io::ErrorKind::InvalidInput`] if `size` is unsupported or +/// [`io::ErrorKind::UnexpectedEof`] if `bytes` is too short. +pub(crate) fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> io::Result { + if bytes.len() < size { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "incomplete length prefix", + )); + } + + let slice = &bytes[..size]; Ok(match (size, endianness) { - (1, _) => u64::from(u8::from_ne_bytes([bytes[0]])), - (2, Endianness::Big) => u64::from(u16::from_be_bytes(bytes[..2].try_into().unwrap())), - (2, Endianness::Little) => u64::from(u16::from_le_bytes(bytes[..2].try_into().unwrap())), - (4, Endianness::Big) => u64::from(u32::from_be_bytes(bytes[..4].try_into().unwrap())), - (4, Endianness::Little) => u64::from(u32::from_le_bytes(bytes[..4].try_into().unwrap())), - (8, Endianness::Big) => u64::from_be_bytes(bytes[..8].try_into().unwrap()), - (8, Endianness::Little) => u64::from_le_bytes(bytes[..8].try_into().unwrap()), + (1, _) => u64::from(u8::from_ne_bytes([slice[0]])), + (2, Endianness::Big) => u64::from(u16::from_be_bytes( + slice.try_into().expect("slice length validated"), + )), + (2, Endianness::Little) => u64::from(u16::from_le_bytes( + slice.try_into().expect("slice length validated"), + )), + (4, Endianness::Big) => u64::from(u32::from_be_bytes( + slice.try_into().expect("slice length validated"), + )), + (4, Endianness::Little) => u64::from(u32::from_le_bytes( + slice.try_into().expect("slice length validated"), + )), + (8, Endianness::Big) => { + u64::from_be_bytes(slice.try_into().expect("slice length validated")) + } + (8, Endianness::Little) => { + u64::from_le_bytes(slice.try_into().expect("slice length validated")) + } _ => { return Err(io::Error::new( io::ErrorKind::InvalidInput, - "unsupported length prefix size", + ERR_UNSUPPORTED_PREFIX, )); } }) } -/// Encodes an integer into bytes according to the given prefix size and -/// endianness. +/// Encodes an integer directly into `out` according to `size` and `endianness`. /// -/// Returns an error if the integer does not fit into the requested size. -fn u64_to_bytes(len: usize, size: usize, endianness: Endianness) -> io::Result> { - Ok(match (size, endianness) { - (1, _) => vec![ - u8::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))?, - ], - (2, Endianness::Big) => u16::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? - .to_be_bytes() - .to_vec(), - (2, Endianness::Little) => u16::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? - .to_le_bytes() - .to_vec(), - (4, Endianness::Big) => u32::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? - .to_be_bytes() - .to_vec(), - (4, Endianness::Little) => u32::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? - .to_le_bytes() - .to_vec(), - (8, Endianness::Big) => u64::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? - .to_be_bytes() - .to_vec(), - (8, Endianness::Little) => u64::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "frame too large"))? - .to_le_bytes() - .to_vec(), +/// The function supports prefix sizes of `1`, `2`, `4`, or `8` bytes. +/// +/// # Errors +/// Returns [`io::ErrorKind::InvalidInput`] if the size is unsupported or if +/// `len` does not fit into the prefix. +pub(crate) fn u64_to_bytes( + len: usize, + size: usize, + endianness: Endianness, + out: &mut [u8; 8], +) -> io::Result { + match (size, endianness) { + (1, _) => { + out[0] = u8::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))?; + } + (2, Endianness::Big) => { + out[..2].copy_from_slice( + &u16::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))? + .to_be_bytes(), + ); + } + (2, Endianness::Little) => { + out[..2].copy_from_slice( + &u16::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))? + .to_le_bytes(), + ); + } + (4, Endianness::Big) => { + out[..4].copy_from_slice( + &u32::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))? + .to_be_bytes(), + ); + } + (4, Endianness::Little) => { + out[..4].copy_from_slice( + &u32::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))? + .to_le_bytes(), + ); + } + (8, Endianness::Big) => { + out[..8].copy_from_slice( + &u64::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))? + .to_be_bytes(), + ); + } + (8, Endianness::Little) => { + out[..8].copy_from_slice( + &u64::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))? + .to_le_bytes(), + ); + } _ => { return Err(io::Error::new( io::ErrorKind::InvalidInput, - "unsupported length prefix size", + ERR_UNSUPPORTED_PREFIX, )); } - }) + } + + Ok(size) } /// Byte order used for encoding and decoding length prefixes. @@ -133,7 +188,7 @@ impl LengthFormat { /// a `usize`. fn read_len(&self, bytes: &[u8]) -> io::Result { let len = bytes_to_u64(bytes, self.bytes, self.endianness)?; - usize::try_from(len).map_err(|_| io::Error::other("frame too large")) + usize::try_from(len).map_err(|_| io::Error::other(ERR_FRAME_TOO_LARGE)) } /// Writes a length prefix to the destination buffer using the configured size and endianness. @@ -149,8 +204,9 @@ impl LengthFormat { /// Returns an error if `len` exceeds the maximum value for the configured prefix size or if the /// prefix size is not supported. fn write_len(&self, len: usize, dst: &mut BytesMut) -> io::Result<()> { - let prefix = u64_to_bytes(len, self.bytes, self.endianness)?; - dst.put_slice(&prefix); + let mut buf = [0u8; 8]; + let written = u64_to_bytes(len, self.bytes, self.endianness, &mut buf)?; + dst.put_slice(&buf[..written]); Ok(()) } } @@ -276,3 +332,62 @@ impl FrameProcessor for LengthPrefixedProcessor { Ok(()) } } + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[rstest] + #[case(vec![0x12], 1, Endianness::Big, 0x12)] + #[case(vec![0x12, 0x34], 2, Endianness::Big, 0x1234)] + #[case(vec![0x34, 0x12], 2, Endianness::Little, 0x1234)] + #[case(vec![0, 0, 0, 1], 4, Endianness::Big, 1)] + #[case(vec![1, 0, 0, 0], 4, Endianness::Little, 1)] + fn bytes_to_u64_ok( + #[case] bytes: Vec, + #[case] size: usize, + #[case] endianness: Endianness, + #[case] expected: u64, + ) { + assert_eq!(bytes_to_u64(&bytes, size, endianness).unwrap(), expected); + } + + #[rstest] + #[case(0x12usize, 1, Endianness::Big, vec![0x12])] + #[case(0x1234usize, 2, Endianness::Big, vec![0x12, 0x34])] + #[case(0x1234usize, 2, Endianness::Little, vec![0x34, 0x12])] + #[case(1usize, 4, Endianness::Big, vec![0, 0, 0, 1])] + #[case(1usize, 4, Endianness::Little, vec![1, 0, 0, 0])] + fn u64_to_bytes_ok( + #[case] value: usize, + #[case] size: usize, + #[case] endianness: Endianness, + #[case] expected: Vec, + ) { + let mut buf = [0u8; 8]; + let written = u64_to_bytes(value, size, endianness, &mut buf).unwrap(); + assert_eq!(written, size); + assert_eq!(&buf[..written], expected.as_slice()); + } + + #[rstest] + #[case(vec![0x01], 2, Endianness::Big)] + #[case(vec![0x02, 0x03], 4, Endianness::Little)] + fn bytes_to_u64_short( + #[case] bytes: Vec, + #[case] size: usize, + #[case] endianness: Endianness, + ) { + let err = bytes_to_u64(&bytes, size, endianness).expect_err("expected error"); + assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof); + } + + #[rstest] + fn u64_to_bytes_large() { + let mut buf = [0u8; 8]; + let err = u64_to_bytes(300, 1, Endianness::Big, &mut buf).expect_err("expected error"); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + } +} From e193ff8a3ebff2b8128618ccf5cb1c99496c5f9f Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 4 Jul 2025 09:40:16 +0100 Subject: [PATCH 03/13] Clarify error context in length helpers --- src/frame.rs | 38 ++++++++++++++++++++++++++------------ tests/response.rs | 10 ++++++---- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/frame.rs b/src/frame.rs index 73a03709..ee3f95ec 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -31,23 +31,35 @@ pub(crate) fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> Ok(match (size, endianness) { (1, _) => u64::from(u8::from_ne_bytes([slice[0]])), (2, Endianness::Big) => u64::from(u16::from_be_bytes( - slice.try_into().expect("slice length validated"), + slice + .try_into() + .expect("expected 2 bytes for u16::from_be_bytes"), )), (2, Endianness::Little) => u64::from(u16::from_le_bytes( - slice.try_into().expect("slice length validated"), + slice + .try_into() + .expect("expected 2 bytes for u16::from_le_bytes"), )), (4, Endianness::Big) => u64::from(u32::from_be_bytes( - slice.try_into().expect("slice length validated"), + slice + .try_into() + .expect("expected 4 bytes for u32::from_be_bytes"), )), (4, Endianness::Little) => u64::from(u32::from_le_bytes( - slice.try_into().expect("slice length validated"), + slice + .try_into() + .expect("expected 4 bytes for u32::from_le_bytes"), )), - (8, Endianness::Big) => { - u64::from_be_bytes(slice.try_into().expect("slice length validated")) - } - (8, Endianness::Little) => { - u64::from_le_bytes(slice.try_into().expect("slice length validated")) - } + (8, Endianness::Big) => u64::from_be_bytes( + slice + .try_into() + .expect("expected 8 bytes for u64::from_be_bytes"), + ), + (8, Endianness::Little) => u64::from_le_bytes( + slice + .try_into() + .expect("expected 8 bytes for u64::from_le_bytes"), + ), _ => { return Err(io::Error::new( io::ErrorKind::InvalidInput, @@ -380,14 +392,16 @@ mod tests { #[case] size: usize, #[case] endianness: Endianness, ) { - let err = bytes_to_u64(&bytes, size, endianness).expect_err("expected error"); + let err = bytes_to_u64(&bytes, size, endianness) + .expect_err("bytes_to_u64 should fail for short slice"); assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof); } #[rstest] fn u64_to_bytes_large() { let mut buf = [0u8; 8]; - let err = u64_to_bytes(300, 1, Endianness::Big, &mut buf).expect_err("expected error"); + let err = u64_to_bytes(300, 1, Endianness::Big, &mut buf) + .expect_err("u64_to_bytes should fail if value exceeds prefix size"); assert_eq!(err.kind(), io::ErrorKind::InvalidInput); } } diff --git a/tests/response.rs b/tests/response.rs index 069334d0..5df5f408 100644 --- a/tests/response.rs +++ b/tests/response.rs @@ -131,7 +131,7 @@ async fn send_response_propagates_write_error() { let err = app .send_response(&mut writer, &TestResp(3)) .await - .expect_err("expected error"); + .expect_err("send_response should propagate write error"); assert!(matches!(err, wireframe::app::SendError::Io(_))); } @@ -142,7 +142,7 @@ fn encode_fails_for_unsupported_prefix_size() { let mut buf = BytesMut::new(); let err = processor .encode(&vec![1, 2], &mut buf) - .expect_err("expected error"); + .expect_err("encode must fail for unsupported prefix size"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); } @@ -151,7 +151,9 @@ fn decode_fails_for_unsupported_prefix_size() { let fmt = LengthFormat::new(3, Endianness::Little); let processor = LengthPrefixedProcessor::new(fmt); let mut buf = BytesMut::from(&[0x00, 0x01, 0x02][..]); - let err = processor.decode(&mut buf).expect_err("expected error"); + let err = processor + .decode(&mut buf) + .expect_err("decode must fail for unsupported prefix size"); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); } @@ -165,6 +167,6 @@ async fn send_response_returns_encode_error() { let err = app .send_response(&mut Vec::new(), &FailingResp) .await - .expect_err("expected error"); + .expect_err("send_response should fail when encode errors"); assert!(matches!(err, wireframe::app::SendError::Serialize(_))); } From dcfe9391abe53428f8493bea4f0cb45568094419 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 4 Jul 2025 19:43:20 +0100 Subject: [PATCH 04/13] Clarify failure reasons in frame tests --- src/frame.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frame.rs b/src/frame.rs index ee3f95ec..e8c4ad56 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -393,7 +393,7 @@ mod tests { #[case] endianness: Endianness, ) { let err = bytes_to_u64(&bytes, size, endianness) - .expect_err("bytes_to_u64 should fail for short slice"); + .expect_err("bytes_to_u64 must error for truncated prefix slice"); assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof); } @@ -401,7 +401,7 @@ mod tests { fn u64_to_bytes_large() { let mut buf = [0u8; 8]; let err = u64_to_bytes(300, 1, Endianness::Big, &mut buf) - .expect_err("u64_to_bytes should fail if value exceeds prefix size"); + .expect_err("u64_to_bytes must fail if value exceeds 1-byte prefix"); assert_eq!(err.kind(), io::ErrorKind::InvalidInput); } } From 94567a467e534f7aad0c12d631f4793464423eb4 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 4 Jul 2025 20:02:45 +0100 Subject: [PATCH 05/13] Refine length helpers after review --- src/frame.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/frame.rs b/src/frame.rs index e8c4ad56..1c6ef50c 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -137,6 +137,8 @@ pub(crate) fn u64_to_bytes( } } + out[size..].fill(0); + Ok(size) } @@ -200,7 +202,8 @@ impl LengthFormat { /// a `usize`. fn read_len(&self, bytes: &[u8]) -> io::Result { let len = bytes_to_u64(bytes, self.bytes, self.endianness)?; - usize::try_from(len).map_err(|_| io::Error::other(ERR_FRAME_TOO_LARGE)) + usize::try_from(len) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE)) } /// Writes a length prefix to the destination buffer using the configured size and endianness. @@ -363,7 +366,10 @@ mod tests { #[case] endianness: Endianness, #[case] expected: u64, ) { - assert_eq!(bytes_to_u64(&bytes, size, endianness).unwrap(), expected); + assert_eq!( + bytes_to_u64(&bytes, size, endianness).expect("bytes_to_u64 should succeed"), + expected + ); } #[rstest] @@ -379,7 +385,8 @@ mod tests { #[case] expected: Vec, ) { let mut buf = [0u8; 8]; - let written = u64_to_bytes(value, size, endianness, &mut buf).unwrap(); + let written = + u64_to_bytes(value, size, endianness, &mut buf).expect("u64_to_bytes should succeed"); assert_eq!(written, size); assert_eq!(&buf[..written], expected.as_slice()); } From c038a88ec3b48033ac5fc71eaf124661b311b029 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 4 Jul 2025 21:37:05 +0100 Subject: [PATCH 06/13] Refine helper docs and error ordering --- src/frame.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/frame.rs b/src/frame.rs index 1c6ef50c..b80b51ec 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -19,7 +19,21 @@ use bytes::{Buf, BufMut, BytesMut}; /// # Errors /// Returns [`io::ErrorKind::InvalidInput`] if `size` is unsupported or /// [`io::ErrorKind::UnexpectedEof`] if `bytes` is too short. +/// +/// # Examples +/// +/// ```rust,no_run,ignore +/// use crate::frame::{Endianness, bytes_to_u64}; +/// let buf = [0x00, 0x10, 0x20, 0x30]; +/// assert_eq!(bytes_to_u64(&buf, 2, Endianness::Big).unwrap(), 0x0010); +/// ``` pub(crate) fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> io::Result { + if !matches!(size, 1 | 2 | 4 | 8) { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + ERR_UNSUPPORTED_PREFIX, + )); + } if bytes.len() < size { return Err(io::Error::new( io::ErrorKind::UnexpectedEof, @@ -29,7 +43,7 @@ pub(crate) fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> let slice = &bytes[..size]; Ok(match (size, endianness) { - (1, _) => u64::from(u8::from_ne_bytes([slice[0]])), + (1, _) => u64::from(slice[0]), (2, Endianness::Big) => u64::from(u16::from_be_bytes( slice .try_into() @@ -76,6 +90,15 @@ pub(crate) fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> /// # Errors /// Returns [`io::ErrorKind::InvalidInput`] if the size is unsupported or if /// `len` does not fit into the prefix. +/// +/// # Examples +/// +/// ```rust,no_run,ignore +/// use crate::frame::{Endianness, u64_to_bytes}; +/// let mut buf = [0u8; 8]; +/// let written = u64_to_bytes(0x1234, 2, Endianness::Big, &mut buf).unwrap(); +/// assert_eq!(&buf[..written], [0x12, 0x34]); +/// ``` pub(crate) fn u64_to_bytes( len: usize, size: usize, From de0cb4a2a2b2bc798f30e175f6a3896818af4012 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 4 Jul 2025 21:47:33 +0100 Subject: [PATCH 07/13] Deduplicate conversion logic Extract cast helper for prefix encoding and centralise incomplete prefix message. --- src/frame.rs | 46 +++++++++++++--------------------------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/src/frame.rs b/src/frame.rs index b80b51ec..c966a60f 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -8,6 +8,11 @@ use std::{convert::TryInto, io}; const ERR_UNSUPPORTED_PREFIX: &str = "unsupported length prefix size"; const ERR_FRAME_TOO_LARGE: &str = "frame too large"; +const ERR_INCOMPLETE_PREFIX: &str = "incomplete length prefix"; + +fn cast>(len: usize) -> io::Result { + T::try_from(len).map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE)) +} use bytes::{Buf, BufMut, BytesMut}; @@ -37,7 +42,7 @@ pub(crate) fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> if bytes.len() < size { return Err(io::Error::new( io::ErrorKind::UnexpectedEof, - "incomplete length prefix", + ERR_INCOMPLETE_PREFIX, )); } @@ -107,50 +112,25 @@ pub(crate) fn u64_to_bytes( ) -> io::Result { match (size, endianness) { (1, _) => { - out[0] = u8::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))?; + out[0] = cast::(len)?; } (2, Endianness::Big) => { - out[..2].copy_from_slice( - &u16::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))? - .to_be_bytes(), - ); + out[..2].copy_from_slice(&cast::(len)?.to_be_bytes()); } (2, Endianness::Little) => { - out[..2].copy_from_slice( - &u16::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))? - .to_le_bytes(), - ); + out[..2].copy_from_slice(&cast::(len)?.to_le_bytes()); } (4, Endianness::Big) => { - out[..4].copy_from_slice( - &u32::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))? - .to_be_bytes(), - ); + out[..4].copy_from_slice(&cast::(len)?.to_be_bytes()); } (4, Endianness::Little) => { - out[..4].copy_from_slice( - &u32::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))? - .to_le_bytes(), - ); + out[..4].copy_from_slice(&cast::(len)?.to_le_bytes()); } (8, Endianness::Big) => { - out[..8].copy_from_slice( - &u64::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))? - .to_be_bytes(), - ); + out[..8].copy_from_slice(&cast::(len)?.to_be_bytes()); } (8, Endianness::Little) => { - out[..8].copy_from_slice( - &u64::try_from(len) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))? - .to_le_bytes(), - ); + out[..8].copy_from_slice(&cast::(len)?.to_le_bytes()); } _ => { return Err(io::Error::new( From 1efe26a1d2d0e83a3657a7f73be62c3446843a13 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 4 Jul 2025 22:08:13 +0100 Subject: [PATCH 08/13] Expand length helper tests for edge cases --- src/frame.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/frame.rs b/src/frame.rs index c966a60f..d84416b3 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -363,6 +363,8 @@ mod tests { #[case(vec![0x34, 0x12], 2, Endianness::Little, 0x1234)] #[case(vec![0, 0, 0, 1], 4, Endianness::Big, 1)] #[case(vec![1, 0, 0, 0], 4, Endianness::Little, 1)] + #[case(vec![0, 0, 0, 0, 0, 0, 0, 1], 8, Endianness::Big, 1)] + #[case(vec![1, 0, 0, 0, 0, 0, 0, 0], 8, Endianness::Little, 1)] fn bytes_to_u64_ok( #[case] bytes: Vec, #[case] size: usize, @@ -381,6 +383,8 @@ mod tests { #[case(0x1234usize, 2, Endianness::Little, vec![0x34, 0x12])] #[case(1usize, 4, Endianness::Big, vec![0, 0, 0, 1])] #[case(1usize, 4, Endianness::Little, vec![1, 0, 0, 0])] + #[case(1usize, 8, Endianness::Big, vec![0, 0, 0, 0, 0, 0, 0, 1])] + #[case(1usize, 8, Endianness::Little, vec![1, 0, 0, 0, 0, 0, 0, 0])] fn u64_to_bytes_ok( #[case] value: usize, #[case] size: usize, @@ -407,6 +411,19 @@ mod tests { assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof); } + #[rstest] + #[case(vec![0x01, 0x02, 0x03], 3, Endianness::Big)] + #[case(vec![0x01, 0x02, 0x03], 3, Endianness::Little)] + fn bytes_to_u64_unsupported( + #[case] bytes: Vec, + #[case] size: usize, + #[case] endianness: Endianness, + ) { + let err = bytes_to_u64(&bytes, size, endianness) + .expect_err("bytes_to_u64 must fail for unsupported size"); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + } + #[rstest] fn u64_to_bytes_large() { let mut buf = [0u8; 8]; @@ -414,4 +431,18 @@ mod tests { .expect_err("u64_to_bytes must fail if value exceeds 1-byte prefix"); assert_eq!(err.kind(), io::ErrorKind::InvalidInput); } + + #[rstest] + #[case(1usize, 3, Endianness::Big)] + #[case(1usize, 3, Endianness::Little)] + fn u64_to_bytes_unsupported( + #[case] value: usize, + #[case] size: usize, + #[case] endianness: Endianness, + ) { + let mut buf = [0u8; 8]; + let err = u64_to_bytes(value, size, endianness, &mut buf) + .expect_err("u64_to_bytes must fail for unsupported size"); + assert_eq!(err.kind(), io::ErrorKind::InvalidInput); + } } From 84f9791f77eeaf972f56e38b966f1773c958207f Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 4 Jul 2025 22:14:05 +0100 Subject: [PATCH 09/13] Add must_use for length prefix helper --- src/frame.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frame.rs b/src/frame.rs index d84416b3..027ee187 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -104,6 +104,7 @@ pub(crate) fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> /// let written = u64_to_bytes(0x1234, 2, Endianness::Big, &mut buf).unwrap(); /// assert_eq!(&buf[..written], [0x12, 0x34]); /// ``` +#[must_use = "length prefix byte count must be used"] pub(crate) fn u64_to_bytes( len: usize, size: usize, From bfcd5de9453c2229576f41b619301a91936424b8 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 4 Jul 2025 22:23:37 +0100 Subject: [PATCH 10/13] Simplify prefix parsing using byteorder --- Cargo.lock | 7 +++ Cargo.toml | 1 + docs/rust-binary-router-library-design.md | 71 ++++++++++++----------- src/frame.rs | 56 +++++------------- 4 files changed, 59 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eba1e08b..7e59d439 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,12 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.10.1" @@ -1199,6 +1205,7 @@ dependencies = [ "async-stream", "async-trait", "bincode", + "byteorder", "bytes", "dashmap", "futures", diff --git a/Cargo.toml b/Cargo.toml index 6e879628..51f430f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ tokio-util = "0.7" futures = "0.3" async-trait = "0.1" bytes = "1" +byteorder = "1" log = "0.4" dashmap = "5" diff --git a/docs/rust-binary-router-library-design.md b/docs/rust-binary-router-library-design.md index fd515843..6c42129d 100644 --- a/docs/rust-binary-router-library-design.md +++ b/docs/rust-binary-router-library-design.md @@ -1123,50 +1123,51 @@ examples are invaluable. They make the abstract design tangible and showcase how 2. **Frame Processor Implementation** (Simple Length-Prefixed Framing using `tokio-util`): - ```rust - // Crate: my_frame_processor.rs - use bytes::{BytesMut, Buf, BufMut}; - use tokio_util::codec::{Encoder, Decoder}; - use std::io; + ```rust + // Crate: my_frame_processor.rs + use bytes::{BytesMut, Buf, BufMut}; + use tokio_util::codec::{Decoder, Encoder}; + use byteorder::{BigEndian, ReadBytesExt}; + use std::io; - pub struct LengthPrefixedCodec; + pub struct LengthPrefixedCodec; - impl Decoder for LengthPrefixedCodec { - type Item = BytesMut; // Raw frame payload - type Error = io::Error; + impl Decoder for LengthPrefixedCodec { + type Item = BytesMut; // Raw frame payload + type Error = io::Error; - fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { - if src.len() < 4 { return Ok(None); } // Not enough data for length prefix + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + if src.len() < 4 { return Ok(None); } // Not enough data for length prefix - let mut length_bytes = [0u8; 4]; - length_bytes.copy_from_slice(&src[..4]); - let length = u32::from_be_bytes(length_bytes) as usize; + let length = (&src[..4]) + .read_u32::() + .expect("slice length checked") as usize; - if src.len() < 4 + length { - src.reserve(4 + length - src.len()); - return Ok(None); // Not enough data for full frame - } + if src.len() < 4 + length { + src.reserve(4 + length - src.len()); + return Ok(None); // Not enough data for full frame + } - src.advance(4); // Consume length prefix - Ok(Some(src.split_to(length))) - } - } + src.advance(4); // Consume length prefix + Ok(Some(src.split_to(length))) + } + } - impl> Encoder for LengthPrefixedCodec { - type Error = io::Error; + impl> Encoder for LengthPrefixedCodec { + type Error = io::Error; - fn encode(&mut self, item: T, dst: &mut BytesMut) -> Result<(), Self::Error> { - let data = item.as_ref(); - dst.reserve(4 + data.len()); - dst.put_u32(data.len() as u32); - dst.put_slice(data); - Ok(()) - } - } - ``` + fn encode(&mut self, item: T, dst: &mut BytesMut) -> Result<(), Self::Error> { + let data = item.as_ref(); + dst.reserve(4 + data.len()); + dst.put_u32(data.len() as u32); + dst.put_slice(data); + Ok(()) + } + } + ``` - (Note: "wireframe" would abstract the direct use of `Encoder`/`Decoder` - behind its own `FrameProcessor` trait or provide helpers.) + (Note: "wireframe" would abstract the direct use of `Encoder`/`Decoder` behind + its own `FrameProcessor` trait or provide helpers.) 1. **Server Setup and Handler**: diff --git a/src/frame.rs b/src/frame.rs index 027ee187..c3ed55c1 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -4,7 +4,9 @@ //! Implementations may use any framing strategy suitable for the //! underlying transport. -use std::{convert::TryInto, io}; +use std::io; + +use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; const ERR_UNSUPPORTED_PREFIX: &str = "unsupported length prefix size"; const ERR_FRAME_TOO_LARGE: &str = "frame too large"; @@ -46,46 +48,18 @@ pub(crate) fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> )); } - let slice = &bytes[..size]; - Ok(match (size, endianness) { - (1, _) => u64::from(slice[0]), - (2, Endianness::Big) => u64::from(u16::from_be_bytes( - slice - .try_into() - .expect("expected 2 bytes for u16::from_be_bytes"), - )), - (2, Endianness::Little) => u64::from(u16::from_le_bytes( - slice - .try_into() - .expect("expected 2 bytes for u16::from_le_bytes"), - )), - (4, Endianness::Big) => u64::from(u32::from_be_bytes( - slice - .try_into() - .expect("expected 4 bytes for u32::from_be_bytes"), - )), - (4, Endianness::Little) => u64::from(u32::from_le_bytes( - slice - .try_into() - .expect("expected 4 bytes for u32::from_le_bytes"), - )), - (8, Endianness::Big) => u64::from_be_bytes( - slice - .try_into() - .expect("expected 8 bytes for u64::from_be_bytes"), - ), - (8, Endianness::Little) => u64::from_le_bytes( - slice - .try_into() - .expect("expected 8 bytes for u64::from_le_bytes"), - ), - _ => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - ERR_UNSUPPORTED_PREFIX, - )); - } - }) + let mut cur = io::Cursor::new(&bytes[..size]); + let val = match (size, endianness) { + (1, _) => cur.read_u8().map(u64::from), + (2, Endianness::Big) => cur.read_u16::().map(u64::from), + (2, Endianness::Little) => cur.read_u16::().map(u64::from), + (4, Endianness::Big) => cur.read_u32::().map(u64::from), + (4, Endianness::Little) => cur.read_u32::().map(u64::from), + (8, Endianness::Big) => cur.read_u64::(), + (8, Endianness::Little) => cur.read_u64::(), + _ => unreachable!(), + }?; + Ok(val) } /// Encodes an integer directly into `out` according to `size` and `endianness`. From afa4a5dd222d36a2bb4b180f382933992625819e Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 4 Jul 2025 22:53:04 +0100 Subject: [PATCH 11/13] Refactor LengthFormat helpers to remove duplication --- src/frame.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frame.rs b/src/frame.rs index c3ed55c1..9b8220d6 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -57,6 +57,7 @@ pub(crate) fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> (4, Endianness::Little) => cur.read_u32::().map(u64::from), (8, Endianness::Big) => cur.read_u64::(), (8, Endianness::Little) => cur.read_u64::(), + // size is validated above so this branch is unreachable _ => unreachable!(), }?; Ok(val) From 7179eb7e97a0906ed7640ac05729d22b209d5b79 Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 4 Jul 2025 23:08:29 +0100 Subject: [PATCH 12/13] Refactor LengthFormat helpers to remove duplication --- docs/rust-binary-router-library-design.md | 6 +++++ src/frame.rs | 31 +++++++++++++---------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/docs/rust-binary-router-library-design.md b/docs/rust-binary-router-library-design.md index 6c42129d..3599733d 100644 --- a/docs/rust-binary-router-library-design.md +++ b/docs/rust-binary-router-library-design.md @@ -1130,6 +1130,8 @@ examples are invaluable. They make the abstract design tangible and showcase how use byteorder::{BigEndian, ReadBytesExt}; use std::io; + const MAX_FRAME_LEN: usize = 16 * 1024 * 1024; // 16 MiB upper limit + pub struct LengthPrefixedCodec; impl Decoder for LengthPrefixedCodec { @@ -1143,6 +1145,10 @@ examples are invaluable. They make the abstract design tangible and showcase how .read_u32::() .expect("slice length checked") as usize; + if length > MAX_FRAME_LEN { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "frame too large")); + } + if src.len() < 4 + length { src.reserve(4 + length - src.len()); return Ok(None); // Not enough data for full frame diff --git a/src/frame.rs b/src/frame.rs index 9b8220d6..3e7b9c58 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -12,7 +12,10 @@ const ERR_UNSUPPORTED_PREFIX: &str = "unsupported length prefix size"; const ERR_FRAME_TOO_LARGE: &str = "frame too large"; const ERR_INCOMPLETE_PREFIX: &str = "incomplete length prefix"; -fn cast>(len: usize) -> io::Result { +/// Checked conversion from `usize` to a specific prefix integer type. +/// +/// Returns `ERR_FRAME_TOO_LARGE` if the value does not fit in `T`. +fn checked_prefix_cast>(len: usize) -> io::Result { T::try_from(len).map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE)) } @@ -29,12 +32,12 @@ use bytes::{Buf, BufMut, BytesMut}; /// /// # Examples /// -/// ```rust,no_run,ignore -/// use crate::frame::{Endianness, bytes_to_u64}; +/// ```rust,no_run +/// use wireframe::frame::{Endianness, bytes_to_u64}; /// let buf = [0x00, 0x10, 0x20, 0x30]; /// assert_eq!(bytes_to_u64(&buf, 2, Endianness::Big).unwrap(), 0x0010); /// ``` -pub(crate) fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> io::Result { +pub fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> io::Result { if !matches!(size, 1 | 2 | 4 | 8) { return Err(io::Error::new( io::ErrorKind::InvalidInput, @@ -73,14 +76,14 @@ pub(crate) fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> /// /// # Examples /// -/// ```rust,no_run,ignore -/// use crate::frame::{Endianness, u64_to_bytes}; +/// ```rust,no_run +/// use wireframe::frame::{Endianness, u64_to_bytes}; /// let mut buf = [0u8; 8]; /// let written = u64_to_bytes(0x1234, 2, Endianness::Big, &mut buf).unwrap(); /// assert_eq!(&buf[..written], [0x12, 0x34]); /// ``` #[must_use = "length prefix byte count must be used"] -pub(crate) fn u64_to_bytes( +pub fn u64_to_bytes( len: usize, size: usize, endianness: Endianness, @@ -88,25 +91,25 @@ pub(crate) fn u64_to_bytes( ) -> io::Result { match (size, endianness) { (1, _) => { - out[0] = cast::(len)?; + out[0] = checked_prefix_cast::(len)?; } (2, Endianness::Big) => { - out[..2].copy_from_slice(&cast::(len)?.to_be_bytes()); + out[..2].copy_from_slice(&checked_prefix_cast::(len)?.to_be_bytes()); } (2, Endianness::Little) => { - out[..2].copy_from_slice(&cast::(len)?.to_le_bytes()); + out[..2].copy_from_slice(&checked_prefix_cast::(len)?.to_le_bytes()); } (4, Endianness::Big) => { - out[..4].copy_from_slice(&cast::(len)?.to_be_bytes()); + out[..4].copy_from_slice(&checked_prefix_cast::(len)?.to_be_bytes()); } (4, Endianness::Little) => { - out[..4].copy_from_slice(&cast::(len)?.to_le_bytes()); + out[..4].copy_from_slice(&checked_prefix_cast::(len)?.to_le_bytes()); } (8, Endianness::Big) => { - out[..8].copy_from_slice(&cast::(len)?.to_be_bytes()); + out[..8].copy_from_slice(&checked_prefix_cast::(len)?.to_be_bytes()); } (8, Endianness::Little) => { - out[..8].copy_from_slice(&cast::(len)?.to_le_bytes()); + out[..8].copy_from_slice(&checked_prefix_cast::(len)?.to_le_bytes()); } _ => { return Err(io::Error::new( From eb3edf533408aa7f75d4737bcb8cad52a50adf7d Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 4 Jul 2025 23:28:58 +0100 Subject: [PATCH 13/13] Refactor LengthFormat helpers to remove duplication --- src/frame.rs | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/frame.rs b/src/frame.rs index 3e7b9c58..0fadf4f1 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -12,6 +12,23 @@ const ERR_UNSUPPORTED_PREFIX: &str = "unsupported length prefix size"; const ERR_FRAME_TOO_LARGE: &str = "frame too large"; const ERR_INCOMPLETE_PREFIX: &str = "incomplete length prefix"; +#[derive(Copy, Clone)] +enum PrefixErr { + UnsupportedSize, + Incomplete, +} + +fn prefix_err(kind: PrefixErr) -> io::Error { + match kind { + PrefixErr::UnsupportedSize => { + io::Error::new(io::ErrorKind::InvalidInput, ERR_UNSUPPORTED_PREFIX) + } + PrefixErr::Incomplete => { + io::Error::new(io::ErrorKind::UnexpectedEof, ERR_INCOMPLETE_PREFIX) + } + } +} + /// Checked conversion from `usize` to a specific prefix integer type. /// /// Returns `ERR_FRAME_TOO_LARGE` if the value does not fit in `T`. @@ -39,16 +56,10 @@ use bytes::{Buf, BufMut, BytesMut}; /// ``` pub fn bytes_to_u64(bytes: &[u8], size: usize, endianness: Endianness) -> io::Result { if !matches!(size, 1 | 2 | 4 | 8) { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - ERR_UNSUPPORTED_PREFIX, - )); + return Err(prefix_err(PrefixErr::UnsupportedSize)); } if bytes.len() < size { - return Err(io::Error::new( - io::ErrorKind::UnexpectedEof, - ERR_INCOMPLETE_PREFIX, - )); + return Err(prefix_err(PrefixErr::Incomplete)); } let mut cur = io::Cursor::new(&bytes[..size]); @@ -112,10 +123,7 @@ pub fn u64_to_bytes( out[..8].copy_from_slice(&checked_prefix_cast::(len)?.to_le_bytes()); } _ => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - ERR_UNSUPPORTED_PREFIX, - )); + return Err(prefix_err(PrefixErr::UnsupportedSize)); } } @@ -311,7 +319,12 @@ impl FrameProcessor for LengthPrefixedProcessor { return Ok(None); } let len = self.format.read_len(&src[..self.format.bytes])?; - if src.len() < self.format.bytes + len { + let needed = self + .format + .bytes + .checked_add(len) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, ERR_FRAME_TOO_LARGE))?; + if src.len() < needed { return Ok(None); } src.advance(self.format.bytes);