Skip to main content

hopper_core/account/
reader.rs

1//! Account reader with header-aware field access.
2
3use super::cursor::SliceCursor;
4use super::header::{AccountHeader, HEADER_LEN};
5use super::pod::pod_from_bytes;
6use hopper_runtime::error::ProgramError;
7
8/// Header-aware read-only account reader.
9///
10/// Provides typed access to the header fields and a `SliceCursor` for the body.
11pub struct AccountReader<'a> {
12    data: &'a [u8],
13}
14
15impl<'a> AccountReader<'a> {
16    /// Create a reader, validating minimum header size.
17    #[inline(always)]
18    pub fn new(data: &'a [u8]) -> Result<Self, ProgramError> {
19        if data.len() < HEADER_LEN {
20            return Err(ProgramError::AccountDataTooSmall);
21        }
22        Ok(Self { data })
23    }
24
25    /// Create a reader with full header validation.
26    #[inline(always)]
27    pub fn new_checked(
28        data: &'a [u8],
29        disc: u8,
30        min_version: u8,
31        layout_id: &[u8; 8],
32    ) -> Result<Self, ProgramError> {
33        super::header::check_header(data, disc, min_version, layout_id)?;
34        Ok(Self { data })
35    }
36
37    /// The underlying header, zero-copy.
38    #[inline(always)]
39    pub fn header(&self) -> &AccountHeader {
40        // SAFETY: We validated data.len() >= HEADER_LEN in constructor.
41        // AccountHeader has align_of == 1 and size == HEADER_LEN.
42        unsafe { &*(self.data.as_ptr() as *const AccountHeader) }
43    }
44
45    /// Discriminator byte.
46    #[inline(always)]
47    pub fn discriminator(&self) -> u8 {
48        self.data[0]
49    }
50
51    /// Version byte.
52    #[inline(always)]
53    pub fn version(&self) -> u8 {
54        self.data[1]
55    }
56
57    /// Flags as u16.
58    #[inline(always)]
59    pub fn flags(&self) -> u16 {
60        u16::from_le_bytes([self.data[2], self.data[3]])
61    }
62
63    /// Layout ID (8 bytes).
64    #[inline(always)]
65    pub fn layout_id(&self) -> [u8; 8] {
66        let mut id = [0u8; 8];
67        id.copy_from_slice(&self.data[4..12]);
68        id
69    }
70
71    /// Schema epoch (u32 LE at bytes 12..16).
72    #[inline(always)]
73    pub fn schema_epoch(&self) -> u32 {
74        u32::from_le_bytes([self.data[12], self.data[13], self.data[14], self.data[15]])
75    }
76
77    /// Body data after the header as a cursor.
78    #[inline(always)]
79    pub fn body(&self) -> SliceCursor<'a> {
80        SliceCursor::new(&self.data[HEADER_LEN..])
81    }
82
83    /// Raw body bytes after the header.
84    #[inline(always)]
85    pub fn body_bytes(&self) -> &'a [u8] {
86        &self.data[HEADER_LEN..]
87    }
88
89    /// Entire raw data including header.
90    #[inline(always)]
91    pub fn raw(&self) -> &'a [u8] {
92        self.data
93    }
94
95    /// Read a u64 at a specific offset from the start of data.
96    #[inline(always)]
97    pub fn u64_at(&self, offset: usize) -> Result<u64, ProgramError> {
98        if offset + 8 > self.data.len() {
99            return Err(ProgramError::InvalidAccountData);
100        }
101        Ok(u64::from_le_bytes([
102            self.data[offset],
103            self.data[offset + 1],
104            self.data[offset + 2],
105            self.data[offset + 3],
106            self.data[offset + 4],
107            self.data[offset + 5],
108            self.data[offset + 6],
109            self.data[offset + 7],
110        ]))
111    }
112
113    /// Read a 32-byte address at a specific offset.
114    #[inline(always)]
115    pub fn address_at(&self, offset: usize) -> Result<&'a [u8; 32], ProgramError> {
116        if offset + 32 > self.data.len() {
117            return Err(ProgramError::InvalidAccountData);
118        }
119        // SAFETY: Checked bounds. [u8; 32] has alignment 1.
120        Ok(unsafe { &*(self.data.as_ptr().add(offset) as *const [u8; 32]) })
121    }
122
123    /// Overlay a Pod type at a specific offset.
124    #[inline(always)]
125    pub fn overlay_at<T: super::Pod + super::FixedLayout>(
126        &self,
127        offset: usize,
128    ) -> Result<&'a T, ProgramError> {
129        if offset > self.data.len() {
130            return Err(ProgramError::InvalidAccountData);
131        }
132        pod_from_bytes::<T>(&self.data[offset..])
133    }
134}