Skip to main content

jiminy_core/account/
collection.rs

1//! Zero-copy dynamic-length collections overlaid on account data.
2//!
3//! Provides [`ZeroCopySlice`] and [`ZeroCopySliceMut`]: length-prefixed
4//! arrays of `Pod` items that read directly from borrowed account bytes
5//! without deserialization or allocation.
6//!
7//! ## On-chain layout
8//!
9//! ```text
10//! [len: u32 LE] [item_0] [item_1] ... [item_{len-1}]
11//! ```
12//!
13//! Each item occupies exactly `T::SIZE` bytes.
14//!
15//! ## Usage
16//!
17//! ```rust,ignore
18//! use jiminy_core::account::collection::{ZeroCopySlice, ZeroCopySliceMut};
19//! use pinocchio::Address;
20//!
21//! // Read a whitelist from account data at some offset:
22//! let whitelist = ZeroCopySlice::<Address>::from_bytes(&data[offset..])?;
23//! let third: &Address = whitelist.get(2)?;
24//!
25//! // Write:
26//! let mut list = ZeroCopySliceMut::<Address>::from_bytes(&mut data[offset..])?;
27//! *list.get_mut(0)? = new_address;
28//! ```
29
30use pinocchio::error::ProgramError;
31
32use super::pod::{FixedLayout, Pod};
33
34/// Length prefix size (u32 LE = 4 bytes).
35const LEN_PREFIX: usize = 4;
36
37/// Immutable zero-copy view over a length-prefixed array in account data.
38///
39/// `T` must implement [`Pod`] + [`FixedLayout`]. No allocations, no copies.
40pub struct ZeroCopySlice<'a, T: Pod + FixedLayout> {
41    len: u32,
42    data: &'a [u8],
43    _marker: core::marker::PhantomData<T>,
44}
45
46impl<'a, T: Pod + FixedLayout> ZeroCopySlice<'a, T> {
47    /// Create a view over `[len: u32][T; len]` at the start of `data`.
48    ///
49    /// Returns `AccountDataTooSmall` if the slice is too short for the
50    /// declared length.
51    #[inline(always)]
52    pub fn from_bytes(data: &'a [u8]) -> Result<Self, ProgramError> {
53        if data.len() < LEN_PREFIX {
54            return Err(ProgramError::AccountDataTooSmall);
55        }
56        let len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
57        let required = LEN_PREFIX + (len as usize) * T::SIZE;
58        if data.len() < required {
59            return Err(ProgramError::AccountDataTooSmall);
60        }
61        Ok(Self {
62            len,
63            data,
64            _marker: core::marker::PhantomData,
65        })
66    }
67
68    /// Number of items in the collection.
69    #[inline(always)]
70    pub fn len(&self) -> u32 {
71        self.len
72    }
73
74    /// Whether the collection is empty.
75    #[inline(always)]
76    pub fn is_empty(&self) -> bool {
77        self.len == 0
78    }
79
80    /// Total byte footprint: 4 (len prefix) + len * T::SIZE.
81    #[inline(always)]
82    pub fn byte_len(&self) -> usize {
83        LEN_PREFIX + (self.len as usize) * T::SIZE
84    }
85
86    /// Get an immutable reference to the item at `index`.
87    ///
88    /// Returns `InvalidArgument` if out of bounds.
89    #[inline(always)]
90    pub fn get(&self, index: u32) -> Result<&T, ProgramError> {
91        if index >= self.len {
92            return Err(ProgramError::InvalidArgument);
93        }
94        let offset = LEN_PREFIX + (index as usize) * T::SIZE;
95        // On SBF alignment is always 1. On native we rely on T::SIZE being
96        // the actual mem size for repr(C) Pod types.
97        #[cfg(target_os = "solana")]
98        {
99            Ok(unsafe { &*(self.data.as_ptr().add(offset) as *const T) })
100        }
101        #[cfg(not(target_os = "solana"))]
102        {
103            let ptr = self.data.as_ptr();
104            if (unsafe { ptr.add(offset) } as usize) % core::mem::align_of::<T>() != 0 {
105                return Err(ProgramError::InvalidAccountData);
106            }
107            Ok(unsafe { &*(ptr.add(offset) as *const T) })
108        }
109    }
110
111    /// Read item at `index` by copy (alignment-safe on all targets).
112    #[inline(always)]
113    pub fn read(&self, index: u32) -> Result<T, ProgramError> {
114        if index >= self.len {
115            return Err(ProgramError::InvalidArgument);
116        }
117        let offset = LEN_PREFIX + (index as usize) * T::SIZE;
118        Ok(unsafe {
119            core::ptr::read_unaligned(self.data.as_ptr().add(offset) as *const T)
120        })
121    }
122
123    /// Iterate over all items as references.
124    #[inline(always)]
125    pub fn iter(&self) -> ZeroCopyIter<'a, T> {
126        ZeroCopyIter {
127            data: self.data,
128            index: 0,
129            len: self.len,
130            _marker: core::marker::PhantomData,
131        }
132    }
133
134    /// Check if `needle` exists in the collection (linear scan).
135    ///
136    /// Compares raw bytes, works for any Pod type.
137    #[inline(always)]
138    pub fn contains_bytes(&self, needle: &[u8]) -> bool {
139        if needle.len() != T::SIZE {
140            return false;
141        }
142        let mut i = 0u32;
143        while i < self.len {
144            let offset = LEN_PREFIX + (i as usize) * T::SIZE;
145            if &self.data[offset..offset + T::SIZE] == needle {
146                return true;
147            }
148            i += 1;
149        }
150        false
151    }
152}
153
154/// Mutable zero-copy view over a length-prefixed array in account data.
155pub struct ZeroCopySliceMut<'a, T: Pod + FixedLayout> {
156    len: u32,
157    data: &'a mut [u8],
158    _marker: core::marker::PhantomData<T>,
159}
160
161impl<'a, T: Pod + FixedLayout> ZeroCopySliceMut<'a, T> {
162    /// Create a mutable view over `[len: u32][T; len]` at the start of `data`.
163    #[inline(always)]
164    pub fn from_bytes(data: &'a mut [u8]) -> Result<Self, ProgramError> {
165        if data.len() < LEN_PREFIX {
166            return Err(ProgramError::AccountDataTooSmall);
167        }
168        let len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
169        let required = LEN_PREFIX + (len as usize) * T::SIZE;
170        if data.len() < required {
171            return Err(ProgramError::AccountDataTooSmall);
172        }
173        Ok(Self {
174            len,
175            data,
176            _marker: core::marker::PhantomData,
177        })
178    }
179
180    /// Create a new collection in `data`, writing `len` as the prefix and
181    /// zeroing the item region.
182    #[inline(always)]
183    pub fn init(data: &'a mut [u8], len: u32) -> Result<Self, ProgramError> {
184        let required = LEN_PREFIX + (len as usize) * T::SIZE;
185        if data.len() < required {
186            return Err(ProgramError::AccountDataTooSmall);
187        }
188        data[0..4].copy_from_slice(&len.to_le_bytes());
189        // Zero the item region (compiles to sol_memset on SBF).
190        let item_region = &mut data[LEN_PREFIX..required];
191        item_region.fill(0);
192        Ok(Self {
193            len,
194            data,
195            _marker: core::marker::PhantomData,
196        })
197    }
198
199    /// Number of items.
200    #[inline(always)]
201    pub fn len(&self) -> u32 {
202        self.len
203    }
204
205    /// Whether the collection is empty.
206    #[inline(always)]
207    pub fn is_empty(&self) -> bool {
208        self.len == 0
209    }
210
211    /// Get a mutable reference to the item at `index`.
212    #[inline(always)]
213    pub fn get_mut(&mut self, index: u32) -> Result<&mut T, ProgramError> {
214        if index >= self.len {
215            return Err(ProgramError::InvalidArgument);
216        }
217        let offset = LEN_PREFIX + (index as usize) * T::SIZE;
218        #[cfg(target_os = "solana")]
219        {
220            Ok(unsafe { &mut *(self.data.as_mut_ptr().add(offset) as *mut T) })
221        }
222        #[cfg(not(target_os = "solana"))]
223        {
224            let ptr = self.data.as_mut_ptr();
225            if (unsafe { ptr.add(offset) } as usize) % core::mem::align_of::<T>() != 0 {
226                return Err(ProgramError::InvalidAccountData);
227            }
228            Ok(unsafe { &mut *(ptr.add(offset) as *mut T) })
229        }
230    }
231
232    /// Get an immutable reference to the item at `index`.
233    #[inline(always)]
234    pub fn get(&self, index: u32) -> Result<&T, ProgramError> {
235        if index >= self.len {
236            return Err(ProgramError::InvalidArgument);
237        }
238        let offset = LEN_PREFIX + (index as usize) * T::SIZE;
239        #[cfg(target_os = "solana")]
240        {
241            Ok(unsafe { &*(self.data.as_ptr().add(offset) as *const T) })
242        }
243        #[cfg(not(target_os = "solana"))]
244        {
245            let ptr = self.data.as_ptr();
246            if (unsafe { ptr.add(offset) } as usize) % core::mem::align_of::<T>() != 0 {
247                return Err(ProgramError::InvalidAccountData);
248            }
249            Ok(unsafe { &*(ptr.add(offset) as *const T) })
250        }
251    }
252
253    /// Write a value at `index` via byte copy (alignment-safe).
254    #[inline(always)]
255    pub fn set(&mut self, index: u32, value: &T) -> Result<(), ProgramError> {
256        if index >= self.len {
257            return Err(ProgramError::InvalidArgument);
258        }
259        let offset = LEN_PREFIX + (index as usize) * T::SIZE;
260        let src = value as *const T as *const u8;
261        unsafe {
262            core::ptr::copy_nonoverlapping(
263                src,
264                self.data.as_mut_ptr().add(offset),
265                T::SIZE,
266            );
267        }
268        Ok(())
269    }
270}
271
272/// Iterator over items in a [`ZeroCopySlice`].
273pub struct ZeroCopyIter<'a, T: Pod + FixedLayout> {
274    data: &'a [u8],
275    index: u32,
276    len: u32,
277    _marker: core::marker::PhantomData<T>,
278}
279
280impl<'a, T: Pod + FixedLayout> Iterator for ZeroCopyIter<'a, T> {
281    type Item = T;
282
283    #[inline(always)]
284    fn next(&mut self) -> Option<Self::Item> {
285        if self.index >= self.len {
286            return None;
287        }
288        let offset = LEN_PREFIX + (self.index as usize) * T::SIZE;
289        self.index += 1;
290        Some(unsafe {
291            core::ptr::read_unaligned(self.data.as_ptr().add(offset) as *const T)
292        })
293    }
294
295    #[inline(always)]
296    fn size_hint(&self) -> (usize, Option<usize>) {
297        let remaining = (self.len - self.index) as usize;
298        (remaining, Some(remaining))
299    }
300}
301
302impl<'a, T: Pod + FixedLayout> ExactSizeIterator for ZeroCopyIter<'a, T> {}