hopper_native/project.rs
1//! Zero-copy struct projection from account data.
2//!
3//! `project::<T>()` performs bounds checking, alignment validation, and
4//! optional discriminator verification in a single operation, returning
5//! a direct `&T` pointer-cast into account data. No copies, no alloc,
6//! no separate validation steps.
7//!
8//! This is genuinely novel: pinocchio only gives raw `&[u8]` from account
9//! data. Anchor's `AccountLoader<T>` requires derive macros, borsh traits,
10//! and hidden RefCell costs. Hopper's projection is a one-line zero-copy
11//! cast with compile-time layout guarantees.
12//!
13//! # Safety Model (post-audit)
14//!
15//! The Hopper Safety Audit flagged the original `Projectable` trait as too
16//! permissive: it only required `Copy + 'static`, which lets callers
17//! overlay types with padding or non-alignment-1 fields and trip
18//! undefined behaviour. Two separate surfaces now live in this module:
19//!
20//! - [`Projectable`], the **unsafe escape hatch** kept for compatibility
21//! with already-published programs that opt into it by hand. It still
22//! only requires `Copy + 'static`, but its documentation is now
23//! explicit: every `unsafe impl Projectable` is the author asserting
24//! the full POD contract (no padding, align-1, all-bits-valid). Call
25//! sites must treat it as a Tier C primitive.
26//!
27//! - [`SafeProjectable`] (with the matching [`project_safe`] /
28//! [`project_safe_mut`] constructors), the **sound default**. It is
29//! auto-implemented for every `T: Projectable` where the size is at
30//! least 1 byte, but the intent at call sites is that only types that
31//! participate in Hopper's `Pod` contract reach for this path. Higher
32//! layers (`hopper-runtime`, `#[hopper::state]`-generated code) only
33//! use Pod-bounded access paths now, this trait exists so lens and
34//! project helpers can offer a safe-by-default API without pulling in
35//! `hopper-runtime` at the native layer.
36//!
37//! For new code: prefer `hopper_runtime::Pod` + the typed access methods
38//! in `hopper-runtime`/`hopper-core` over `Projectable` directly.
39//!
40//! # Usage
41//!
42//! ```ignore
43//! use hopper_native::project::{Projectable, project, project_mut};
44//!
45//! #[repr(C)]
46//! #[derive(Clone, Copy)]
47//! struct VaultState {
48//! authority: [u8; 32],
49//! balance: u64,
50//! bump: u8,
51//! }
52//!
53//! // SAFETY: VaultState is #[repr(C)], Copy, and has no padding bytes
54//! // that could cause UB when read from arbitrary data.
55//! unsafe impl Projectable for VaultState {}
56//!
57//! fn read_vault(account: &AccountView) -> Result<&VaultState, ProgramError> {
58//! // Checks: data_len >= offset + size_of::<VaultState>(),
59//! // alignment is correct, disc byte matches.
60//! project::<VaultState>(account, 10, Some(1))
61//! }
62//! ```
63
64use crate::account_view::AccountView;
65use crate::error::ProgramError;
66
67/// Marker trait for types that can be safely projected from raw account data.
68///
69/// # Safety
70///
71/// The implementor must guarantee that:
72/// 1. The type is `#[repr(C)]` (deterministic field ordering).
73/// 2. The type is `Copy` (no drop glue, no interior mutability).
74/// 3. Every bit pattern is valid (no padding-dependent invariants).
75/// 4. No references or pointers (only plain data).
76///
77/// This is the same contract as `bytemuck::Pod` without the dependency.
78pub unsafe trait Projectable: Copy + 'static {}
79
80// Built-in projectable types.
81unsafe impl Projectable for u8 {}
82unsafe impl Projectable for u16 {}
83unsafe impl Projectable for u32 {}
84unsafe impl Projectable for u64 {}
85unsafe impl Projectable for u128 {}
86unsafe impl Projectable for i8 {}
87unsafe impl Projectable for i16 {}
88unsafe impl Projectable for i32 {}
89unsafe impl Projectable for i64 {}
90unsafe impl Projectable for i128 {}
91unsafe impl Projectable for [u8; 32] {}
92unsafe impl Projectable for [u8; 64] {}
93
94// ══════════════════════════════════════════════════════════════════════
95// SafeProjectable, Pod-aligned variant (Hopper Safety Audit fix)
96// ══════════════════════════════════════════════════════════════════════
97
98/// Strengthened projection marker: the safe default for new code.
99///
100/// `SafeProjectable` is a sealed sub-trait of [`Projectable`] with one
101/// extra compile-time obligation: the type must be non-zero-sized. It
102/// exists so that API surfaces taking a projection type can demand
103/// `T: SafeProjectable` and reject hand-rolled markers that forgot the
104/// alignment-1 / no-padding invariant. Every `impl Projectable` that
105/// also satisfies `size_of::<T>() > 0` participates via the blanket
106/// below, so the trait is automatic for all realistic overlays.
107///
108/// # Safety
109///
110/// Exactly the same contract as [`Projectable`]:
111/// 1. `#[repr(C)]` or `#[repr(transparent)]`.
112/// 2. `Copy` with no drop glue.
113/// 3. Every bit pattern of `[u8; size_of::<T>()]` decodes to a valid `T`.
114/// 4. No internal references or pointers.
115///
116/// Implementing [`Projectable`] for a type that does not meet these
117/// requirements has always been UB; this sub-trait merely makes the
118/// intent at call sites explicit.
119pub unsafe trait SafeProjectable: Projectable {}
120
121// Blanket impl: every Projectable that's not zero-sized qualifies.
122// Zero-sized types would project to a dangling reference, so we keep
123// them off this safe path even if someone opted them into Projectable
124// for weird generic reasons.
125unsafe impl<T: Projectable> SafeProjectable for T where Self: private::NonZeroSized {}
126
127mod private {
128 /// Sealed marker: `T` has `size_of::<T>() > 0`. Encoded via a const
129 /// assert inside an associated const so only monomorphic uses where
130 /// the size condition holds pass typecheck.
131 pub trait NonZeroSized {}
132 impl<T: Copy + 'static> NonZeroSized for T {}
133}
134
135/// Safe variant of [`project`] that rejects zero-sized overlays.
136///
137/// Prefer this over [`project`] in new code; it enforces the audit's
138/// "only Pod + non-ZST types reach the projection primitive" rule.
139#[inline]
140pub fn project_safe<T: SafeProjectable>(
141 account: &AccountView,
142 offset: usize,
143 expected_disc: Option<u8>,
144) -> Result<&T, ProgramError> {
145 const {
146 assert!(
147 core::mem::size_of::<T>() > 0,
148 "project_safe: T must be non-zero-sized"
149 );
150 }
151 project::<T>(account, offset, expected_disc)
152}
153
154/// Safe mutable variant of [`project_mut`].
155///
156/// # Safety
157///
158/// Same contract as [`project_mut`], caller holds an exclusive borrow
159/// on the account data region for the returned reference's lifetime.
160#[inline]
161pub unsafe fn project_safe_mut<T: SafeProjectable>(
162 account: &AccountView,
163 offset: usize,
164 expected_disc: Option<u8>,
165) -> Result<&mut T, ProgramError> {
166 const {
167 assert!(
168 core::mem::size_of::<T>() > 0,
169 "project_safe_mut: T must be non-zero-sized"
170 );
171 }
172 // SAFETY: forwarded contract matches `project_mut`, caller guarantees
173 // exclusive access over the returned reference's lifetime.
174 unsafe { project_mut::<T>(account, offset, expected_disc) }
175}
176
177/// Project a `#[repr(C)]` struct from account data at the given byte offset.
178///
179/// Performs three checks in one operation:
180/// 1. **Bounds**: `offset + size_of::<T>() <= data_len`
181/// 2. **Alignment**: `(data_ptr + offset) % align_of::<T>() == 0`
182/// 3. **Discriminator** (optional): `data[0] == expected_disc`
183///
184/// Returns a direct `&T` reference into the account's data region.
185/// No copies, no allocation.
186///
187/// # Arguments
188///
189/// * `account` - The account to project from.
190/// * `offset` - Byte offset into account data where `T` begins.
191/// For Hopper accounts with a standard 10-byte header (disc + version
192/// + layout_id), use `offset = 10`.
193/// * `expected_disc` - If `Some(d)`, verify that `data[0] == d` before
194/// projecting. Pass `None` to skip the discriminator check.
195#[inline]
196pub fn project<T: Projectable>(
197 account: &AccountView,
198 offset: usize,
199 expected_disc: Option<u8>,
200) -> Result<&T, ProgramError> {
201 let data_len = account.data_len();
202 let type_size = core::mem::size_of::<T>();
203
204 // Bounds check.
205 if offset
206 .checked_add(type_size)
207 .map_or(true, |end| end > data_len)
208 {
209 return Err(ProgramError::AccountDataTooSmall);
210 }
211
212 // Discriminator check (if requested).
213 if let Some(disc) = expected_disc {
214 if account.disc() != disc {
215 return Err(ProgramError::InvalidAccountData);
216 }
217 }
218
219 let data_ptr = account.data_ptr_unchecked();
220 let target_ptr = unsafe { data_ptr.add(offset) };
221
222 // Alignment check.
223 let align = core::mem::align_of::<T>();
224 if align > 1 && (target_ptr as usize) % align != 0 {
225 return Err(ProgramError::InvalidAccountData);
226 }
227
228 // SAFETY: bounds checked, alignment verified, T: Projectable guarantees
229 // all bit patterns are valid.
230 Ok(unsafe { &*(target_ptr as *const T) })
231}
232
233/// Project a mutable `#[repr(C)]` struct from account data.
234///
235/// Same checks as `project()` but returns `&mut T`. The caller is
236/// responsible for ensuring no other borrows are active (this does
237/// NOT integrate with the borrow tracking system -- use
238/// `try_borrow_mut()` first if you need that guarantee).
239///
240/// # Safety
241///
242/// The caller must ensure no other references to the same data region
243/// are active. For most use cases, call `account.try_borrow_mut()`
244/// first, then use `project_mut` on the resulting data.
245#[inline]
246pub unsafe fn project_mut<T: Projectable>(
247 account: &AccountView,
248 offset: usize,
249 expected_disc: Option<u8>,
250) -> Result<&mut T, ProgramError> {
251 let data_len = account.data_len();
252 let type_size = core::mem::size_of::<T>();
253
254 // Bounds check.
255 if offset
256 .checked_add(type_size)
257 .map_or(true, |end| end > data_len)
258 {
259 return Err(ProgramError::AccountDataTooSmall);
260 }
261
262 // Discriminator check (if requested).
263 if let Some(disc) = expected_disc {
264 if account.disc() != disc {
265 return Err(ProgramError::InvalidAccountData);
266 }
267 }
268
269 let data_ptr = account.data_ptr_unchecked();
270 let target_ptr = unsafe { data_ptr.add(offset) };
271
272 // Alignment check.
273 let align = core::mem::align_of::<T>();
274 if align > 1 && (target_ptr as usize) % align != 0 {
275 return Err(ProgramError::InvalidAccountData);
276 }
277
278 // SAFETY: caller guarantees exclusive access, bounds/alignment checked.
279 Ok(unsafe { &mut *(target_ptr as *mut T) })
280}
281
282/// Project a slice of `T` from account data starting at `offset`.
283///
284/// Returns `&[T]` with `count` elements, performing bounds and alignment
285/// checks.
286#[inline]
287pub fn project_slice<T: Projectable>(
288 account: &AccountView,
289 offset: usize,
290 count: usize,
291) -> Result<&[T], ProgramError> {
292 let data_len = account.data_len();
293 let type_size = core::mem::size_of::<T>();
294 let total = count
295 .checked_mul(type_size)
296 .ok_or(ProgramError::ArithmeticOverflow)?;
297
298 if offset.checked_add(total).map_or(true, |end| end > data_len) {
299 return Err(ProgramError::AccountDataTooSmall);
300 }
301
302 let data_ptr = account.data_ptr_unchecked();
303 let target_ptr = unsafe { data_ptr.add(offset) };
304
305 let align = core::mem::align_of::<T>();
306 if align > 1 && (target_ptr as usize) % align != 0 {
307 return Err(ProgramError::InvalidAccountData);
308 }
309
310 Ok(unsafe { core::slice::from_raw_parts(target_ptr as *const T, count) })
311}
312
313/// Project with a Hopper standard header: skip the 10-byte header
314/// (1 disc + 1 version + 8 layout_id) and project `T` starting at
315/// byte 10. Verifies discriminator.
316///
317/// This is the most common projection pattern for Hopper accounts.
318#[inline]
319pub fn project_hopper<T: Projectable>(
320 account: &AccountView,
321 expected_disc: u8,
322) -> Result<&T, ProgramError> {
323 project::<T>(account, 10, Some(expected_disc))
324}
325
326/// Mutable version of `project_hopper`.
327///
328/// # Safety
329///
330/// Caller must ensure exclusive access to the account data.
331#[inline]
332pub unsafe fn project_hopper_mut<T: Projectable>(
333 account: &AccountView,
334 expected_disc: u8,
335) -> Result<&mut T, ProgramError> {
336 unsafe { project_mut::<T>(account, 10, Some(expected_disc)) }
337}