use crate::DeserializeRevisioned;
use crate::Error;
use crate::optimised::validation::validate_struct_prologue;
#[derive(Debug)]
pub struct IndexedStructWalker<'p> {
payload: &'p [u8],
field_count: u16,
revision: u16,
}
impl<'p> IndexedStructWalker<'p> {
pub fn from_payload(payload: &'p [u8], revision: u16, field_count: u16) -> Result<Self, Error> {
let prologue_bytes = (field_count as usize) * 4;
if payload.len() < prologue_bytes {
return Err(Error::OptimisedSubReaderOverrun);
}
let count = field_count as usize;
validate_struct_prologue(&payload[..prologue_bytes], count, 4, payload.len() as u32)?;
if count > 0 {
let first = crate::optimised::validation::decode_u32_le_at(payload, 0);
if (first as usize) < prologue_bytes {
return Err(Error::OptimisedOffsetOutOfRange {
offset: first,
payload_len: prologue_bytes as u32,
});
}
}
Ok(Self {
payload,
field_count,
revision,
})
}
pub fn from_payload_unvalidated(
payload: &'p [u8],
revision: u16,
field_count: u16,
) -> Result<Self, Error> {
let prologue_bytes = (field_count as usize) * 4;
if payload.len() < prologue_bytes {
return Err(Error::OptimisedSubReaderOverrun);
}
Ok(Self {
payload,
field_count,
revision,
})
}
#[inline]
pub fn revision(&self) -> u16 {
self.revision
}
#[inline]
pub fn field_count(&self) -> u16 {
self.field_count
}
pub fn field_bytes(&self, index: u16) -> Result<&'p [u8], Error> {
let i = index as usize;
if i >= self.field_count as usize {
return Err(Error::Deserialize(format!(
"field index {i} out of range ({})",
self.field_count
)));
}
let start = self.offset(i) as usize;
let end = if i + 1 < self.field_count as usize {
self.offset(i + 1) as usize
} else {
self.payload.len()
};
Ok(&self.payload[start..end])
}
pub fn decode_field<T: DeserializeRevisioned>(&self, index: u16) -> Result<T, Error> {
let mut bytes = self.field_bytes(index)?;
T::deserialize_revisioned(&mut bytes)
}
#[inline]
pub fn skip_field(&self, _index: u16) -> Result<(), Error> {
Ok(())
}
#[inline]
fn offset(&self, index: usize) -> u32 {
let start = index * 4;
let bytes = &self.payload[start..start + 4];
u32::from_le_bytes(bytes.try_into().expect("4-byte slice"))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_struct_payload(field_data: &[&[u8]]) -> Vec<u8> {
let field_count = field_data.len();
let prologue_bytes = field_count * 4;
let mut offsets = Vec::with_capacity(field_count);
let mut running = prologue_bytes as u32;
for f in field_data {
offsets.push(running);
running += f.len() as u32;
}
let mut out = Vec::with_capacity(running as usize);
for o in &offsets {
out.extend_from_slice(&o.to_le_bytes());
}
for f in field_data {
out.extend_from_slice(f);
}
out
}
#[test]
fn opens_and_reads_field_bytes_in_order() {
let payload = build_struct_payload(&[b"alpha", b"beta", b"gamma"]);
let w = IndexedStructWalker::from_payload(&payload, 2, 3).unwrap();
assert_eq!(w.field_count(), 3);
assert_eq!(w.revision(), 2);
assert_eq!(w.field_bytes(0).unwrap(), b"alpha");
assert_eq!(w.field_bytes(1).unwrap(), b"beta");
assert_eq!(w.field_bytes(2).unwrap(), b"gamma");
}
#[test]
fn rejects_out_of_range_field() {
let payload = build_struct_payload(&[b"a", b"b"]);
let w = IndexedStructWalker::from_payload(&payload, 1, 2).unwrap();
assert!(w.field_bytes(2).is_err());
}
#[test]
fn rejects_truncated_prologue() {
let payload = [0u8, 0, 0]; let err = IndexedStructWalker::from_payload(&payload, 1, 2).unwrap_err();
assert!(matches!(err, Error::OptimisedSubReaderOverrun));
}
#[test]
fn rejects_offset_out_of_range() {
let mut payload = vec![0u8; 8];
payload[0..4].copy_from_slice(&100u32.to_le_bytes());
let err = IndexedStructWalker::from_payload(&payload, 1, 1).unwrap_err();
assert!(matches!(err, Error::OptimisedOffsetOutOfRange { .. }));
}
#[test]
fn rejects_non_monotonic_offsets() {
let mut payload = vec![0u8; 8 + 16];
payload[0..4].copy_from_slice(&16u32.to_le_bytes());
payload[4..8].copy_from_slice(&8u32.to_le_bytes());
let err = IndexedStructWalker::from_payload(&payload, 1, 2).unwrap_err();
assert!(matches!(err, Error::OptimisedOffsetsNonMonotonic));
}
#[test]
fn rejects_first_offset_before_prologue_end() {
let mut payload = vec![0u8; 8];
payload[0..4].copy_from_slice(&2u32.to_le_bytes());
let err = IndexedStructWalker::from_payload(&payload, 1, 1).unwrap_err();
assert!(matches!(err, Error::OptimisedOffsetOutOfRange { .. }));
}
}