Skip to main content

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}