use hopper_schema::{FieldDescriptor, LayoutManifest};
use crate::fingerprint::{
check_against_layout, FingerprintCheck, FingerprintError, LAYOUT_ID_OFFSET,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReaderError {
BufferTooShort {
required: usize,
got: usize,
},
LayoutMismatch {
expected: [u8; 8],
actual: [u8; 8],
},
UnknownField,
SizeMismatch {
wire: u16,
requested: usize,
},
Fingerprint(FingerprintError),
}
impl From<FingerprintError> for ReaderError {
fn from(e: FingerprintError) -> Self {
ReaderError::Fingerprint(e)
}
}
#[derive(Debug)]
pub struct SegmentReader<'a> {
bytes: &'a [u8],
layout: &'a LayoutManifest,
}
impl<'a> SegmentReader<'a> {
pub fn new(bytes: &'a [u8], layout: &'a LayoutManifest) -> Result<Self, ReaderError> {
if bytes.len() < layout.total_size {
return Err(ReaderError::BufferTooShort {
required: layout.total_size,
got: bytes.len(),
});
}
match check_against_layout(bytes, layout)? {
FingerprintCheck::Match => Ok(Self { bytes, layout }),
FingerprintCheck::Mismatch { expected, actual } => {
Err(ReaderError::LayoutMismatch { expected, actual })
}
}
}
pub unsafe fn new_unchecked(bytes: &'a [u8], layout: &'a LayoutManifest) -> Self {
Self { bytes, layout }
}
pub const fn bytes(&self) -> &'a [u8] {
self.bytes
}
pub const fn layout(&self) -> &'a LayoutManifest {
self.layout
}
pub fn field(&self, name: &str) -> Option<&'a FieldDescriptor> {
let mut i = 0;
while i < self.layout.fields.len() {
if bytes_eq(self.layout.fields[i].name, name) {
return Some(&self.layout.fields[i]);
}
i += 1;
}
None
}
pub fn offset_of(&self, name: &str) -> Option<usize> {
self.field(name).map(|f| f.offset as usize)
}
pub fn read_raw(&self, name: &str) -> Result<&'a [u8], ReaderError> {
let f = self.field(name).ok_or(ReaderError::UnknownField)?;
let start = f.offset as usize;
let end = start + f.size as usize;
if end > self.bytes.len() {
return Err(ReaderError::BufferTooShort {
required: end,
got: self.bytes.len(),
});
}
Ok(&self.bytes[start..end])
}
pub fn read_u8(&self, name: &str) -> Result<u8, ReaderError> {
let raw = self.read_fixed(name, 1)?;
Ok(raw[0])
}
pub fn read_u16(&self, name: &str) -> Result<u16, ReaderError> {
let raw = self.read_fixed(name, 2)?;
Ok(u16::from_le_bytes([raw[0], raw[1]]))
}
pub fn read_u32(&self, name: &str) -> Result<u32, ReaderError> {
let raw = self.read_fixed(name, 4)?;
Ok(u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]))
}
pub fn read_u64(&self, name: &str) -> Result<u64, ReaderError> {
let raw = self.read_fixed(name, 8)?;
Ok(u64::from_le_bytes([
raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7],
]))
}
pub fn read_u128(&self, name: &str) -> Result<u128, ReaderError> {
let raw = self.read_fixed(name, 16)?;
let mut bytes = [0u8; 16];
bytes.copy_from_slice(raw);
Ok(u128::from_le_bytes(bytes))
}
pub fn read_pubkey(&self, name: &str) -> Result<[u8; 32], ReaderError> {
let raw = self.read_fixed(name, 32)?;
let mut out = [0u8; 32];
out.copy_from_slice(raw);
Ok(out)
}
pub fn layout_id(&self) -> [u8; 8] {
let mut id = [0u8; 8];
id.copy_from_slice(&self.bytes[LAYOUT_ID_OFFSET..LAYOUT_ID_OFFSET + 8]);
id
}
fn read_fixed(&self, name: &str, expect: usize) -> Result<&'a [u8], ReaderError> {
let f = self.field(name).ok_or(ReaderError::UnknownField)?;
if f.size as usize != expect {
return Err(ReaderError::SizeMismatch {
wire: f.size,
requested: expect,
});
}
let start = f.offset as usize;
let end = start + expect;
if end > self.bytes.len() {
return Err(ReaderError::BufferTooShort {
required: end,
got: self.bytes.len(),
});
}
Ok(&self.bytes[start..end])
}
}
fn bytes_eq(a: &str, b: &str) -> bool {
let a = a.as_bytes();
let b = b.as_bytes();
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use hopper_schema::FieldIntent;
const LAYOUT_ID: [u8; 8] = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22];
fn fields() -> &'static [FieldDescriptor] {
static F: [FieldDescriptor; 3] = [
FieldDescriptor {
name: "authority",
canonical_type: "Pubkey",
size: 32,
offset: 16,
intent: FieldIntent::Authority,
},
FieldDescriptor {
name: "balance",
canonical_type: "u64",
size: 8,
offset: 48,
intent: FieldIntent::Balance,
},
FieldDescriptor {
name: "bump",
canonical_type: "u8",
size: 1,
offset: 56,
intent: FieldIntent::Bump,
},
];
&F
}
fn manifest() -> LayoutManifest {
LayoutManifest {
name: "Vault",
disc: 5,
version: 1,
layout_id: LAYOUT_ID,
total_size: 80,
field_count: 3,
fields: fields(),
}
}
fn blob() -> [u8; 80] {
let mut b = [0u8; 80];
b[0] = 5;
b[1] = 1;
b[LAYOUT_ID_OFFSET..LAYOUT_ID_OFFSET + 8].copy_from_slice(&LAYOUT_ID);
for i in 0..32 {
b[16 + i] = i as u8;
}
b[48..56].copy_from_slice(&1_000_000u64.to_le_bytes());
b[56] = 253;
b
}
#[test]
fn binds_and_reads() {
let m = manifest();
let b = blob();
let r = SegmentReader::new(&b, &m).unwrap();
assert_eq!(r.read_u64("balance").unwrap(), 1_000_000);
assert_eq!(r.read_u8("bump").unwrap(), 253);
let pk = r.read_pubkey("authority").unwrap();
assert_eq!(pk[0], 0);
assert_eq!(pk[31], 31);
}
#[test]
fn rejects_wrong_layout_id() {
let m = manifest();
let mut b = blob();
b[LAYOUT_ID_OFFSET] ^= 0xFF;
let err = SegmentReader::new(&b, &m).unwrap_err();
assert!(matches!(err, ReaderError::LayoutMismatch { .. }));
}
#[test]
fn rejects_too_short() {
let m = manifest();
let b = [0u8; 40];
let err = SegmentReader::new(&b, &m).unwrap_err();
assert!(matches!(
err,
ReaderError::BufferTooShort {
required: 80,
got: 40
}
));
}
#[test]
fn size_mismatch_detected() {
let m = manifest();
let b = blob();
let r = SegmentReader::new(&b, &m).unwrap();
assert!(matches!(
r.read_u32("balance"),
Err(ReaderError::SizeMismatch {
wire: 8,
requested: 4
})
));
}
}