use crate::error::{Error, Result};
use crate::transport::align_up;
pub(super) const PTR_SIZE: usize = std::mem::size_of::<usize>();
pub(super) struct Cursor<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
pub(super) fn new(buf: &'a [u8]) -> Self {
Self { buf, pos: 0 }
}
fn need(&self, n: usize) -> Result<()> {
if self
.pos
.checked_add(n)
.is_none_or(|end| end > self.buf.len())
{
return Err(Error::Protocol(format!(
"truncated message: need {n} more bytes at offset {} (have {})",
self.pos,
self.buf.len()
)));
}
Ok(())
}
fn read_raw(&mut self, n: usize) -> Result<&'a [u8]> {
self.need(n)?;
let bytes = &self.buf[self.pos..self.pos + n];
self.pos += n;
Ok(bytes)
}
fn chunk_header(&mut self) -> Result<(usize, usize)> {
let pad = align_up(self.pos, PTR_SIZE) - self.pos;
self.pos += pad;
let header_len = 2 * PTR_SIZE;
self.need(header_len)?;
let orig = read_native_usize(&self.buf[self.pos..self.pos + PTR_SIZE]);
let size = read_native_usize(&self.buf[self.pos + PTR_SIZE..self.pos + header_len]);
self.pos += header_len;
Ok((orig, size))
}
fn leaf(&mut self) -> Result<(usize, &'a [u8])> {
let (orig, declared_size) = self.chunk_header()?;
let header_len = 2 * PTR_SIZE;
let content_len = declared_size.checked_sub(header_len).ok_or_else(|| {
Error::Protocol(format!(
"chunk declares {declared_size} bytes, smaller than its own {header_len}-byte header"
))
})?;
Ok((orig, self.read_raw(content_len)?))
}
}
fn read_native_usize(bytes: &[u8]) -> usize {
let mut b = [0u8; 8];
b[..PTR_SIZE].copy_from_slice(bytes);
u64::from_ne_bytes(b) as usize
}
pub(super) fn read_pod<T: Copy>(bytes: &[u8]) -> Result<T> {
if bytes.len() < std::mem::size_of::<T>() {
return Err(Error::Protocol(format!(
"chunk too small: need {} bytes, got {}",
std::mem::size_of::<T>(),
bytes.len()
)));
}
Ok(unsafe { std::ptr::read_unaligned(bytes.as_ptr().cast()) })
}
pub(super) fn read_chunk_pod<T: Copy>(cursor: &mut Cursor) -> Result<T> {
let (_orig, declared_size) = cursor.chunk_header()?;
let header_len = 2 * PTR_SIZE;
let n = std::mem::size_of::<T>();
if declared_size < header_len + n {
return Err(Error::Protocol(format!(
"chunk declares {declared_size} bytes, too small for its own {header_len}-byte header plus the {n}-byte struct it contains"
)));
}
read_pod(cursor.read_raw(n)?)
}
pub(super) fn read_opt_cstring(cursor: &mut Cursor, ptr_field: usize) -> Result<Option<String>> {
if ptr_field == 0 {
return Ok(None);
}
let (_orig, body) = cursor.leaf()?;
let bytes = body.strip_suffix(&[0u8]).unwrap_or(body);
Ok(Some(String::from_utf8_lossy(bytes).into_owned()))
}
pub(super) fn read_opt_bytes(cursor: &mut Cursor, ptr_field: usize) -> Result<Option<Vec<u8>>> {
if ptr_field == 0 {
return Ok(None);
}
let (_orig, body) = cursor.leaf()?;
Ok(Some(body.to_vec()))
}
pub(super) fn consume_substruct_marker(cursor: &mut Cursor) -> Result<()> {
cursor.chunk_header()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn chunk_bytes(orig: usize, body: &[u8]) -> Vec<u8> {
let header_len = 2 * PTR_SIZE;
let mut out = orig.to_ne_bytes().to_vec();
out.extend_from_slice(&(header_len + body.len()).to_ne_bytes());
out.extend_from_slice(body);
out
}
#[test]
fn reads_a_single_chunk() {
let buf = chunk_bytes(1, b"hi");
let mut c = Cursor::new(&buf);
let (orig, body) = c.leaf().unwrap();
assert_eq!(orig, 1);
assert_eq!(body, b"hi");
}
#[test]
fn pads_between_chunks_to_pointer_alignment() {
let mut buf = chunk_bytes(1, b"x"); let second = chunk_bytes(2, b"second");
buf.extend_from_slice(&second);
let mut padded = chunk_bytes(1, b"x");
let pad = (PTR_SIZE - padded.len() % PTR_SIZE) % PTR_SIZE;
padded.extend(std::iter::repeat_n(0u8, pad));
padded.extend_from_slice(&second);
let mut c = Cursor::new(&padded);
let (orig1, body1) = c.leaf().unwrap();
assert_eq!((orig1, body1), (1, &b"x"[..]));
let (orig2, body2) = c.leaf().unwrap();
assert_eq!((orig2, body2), (2, &b"second"[..]));
}
#[test]
fn opt_cstring_strips_trailing_nul() {
let buf = chunk_bytes(1, b"hello\0");
let mut c = Cursor::new(&buf);
assert_eq!(
read_opt_cstring(&mut c, 42).unwrap(),
Some("hello".to_string())
);
}
#[test]
fn opt_cstring_null_pointer_reads_nothing() {
let mut c = Cursor::new(&[]);
assert_eq!(read_opt_cstring(&mut c, 0).unwrap(), None);
}
#[test]
fn truncated_message_is_a_protocol_error() {
let mut c = Cursor::new(&[0u8; 3]);
assert!(c.leaf().is_err());
}
}