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    // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
221    let target_ptr = unsafe { data_ptr.add(offset) };
222
223    // Alignment check.
224    let align = core::mem::align_of::<T>();
225    if align > 1 && (target_ptr as usize) % align != 0 {
226        return Err(ProgramError::InvalidAccountData);
227    }
228
229    // SAFETY: bounds checked, alignment verified, T: Projectable guarantees
230    // all bit patterns are valid.
231    Ok(unsafe { &*(target_ptr as *const T) })
232}
233
234/// Project a mutable `#[repr(C)]` struct from account data.
235///
236/// Same checks as `project()` but returns `&mut T`. The caller is
237/// responsible for ensuring no other borrows are active (this does
238/// NOT integrate with the borrow tracking system -- use
239/// `try_borrow_mut()` first if you need that guarantee).
240///
241/// # Safety
242///
243/// The caller must ensure no other references to the same data region
244/// are active. For most use cases, call `account.try_borrow_mut()`
245/// first, then use `project_mut` on the resulting data.
246#[inline]
247pub unsafe fn project_mut<T: Projectable>(
248    account: &AccountView,
249    offset: usize,
250    expected_disc: Option<u8>,
251) -> Result<&mut T, ProgramError> {
252    let data_len = account.data_len();
253    let type_size = core::mem::size_of::<T>();
254
255    // Bounds check.
256    if offset
257        .checked_add(type_size)
258        .map_or(true, |end| end > data_len)
259    {
260        return Err(ProgramError::AccountDataTooSmall);
261    }
262
263    // Discriminator check (if requested).
264    if let Some(disc) = expected_disc {
265        if account.disc() != disc {
266            return Err(ProgramError::InvalidAccountData);
267        }
268    }
269
270    let data_ptr = account.data_ptr_unchecked();
271    // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
272    let target_ptr = unsafe { data_ptr.add(offset) };
273
274    // Alignment check.
275    let align = core::mem::align_of::<T>();
276    if align > 1 && (target_ptr as usize) % align != 0 {
277        return Err(ProgramError::InvalidAccountData);
278    }
279
280    // SAFETY: caller guarantees exclusive access, bounds/alignment checked.
281    Ok(unsafe { &mut *(target_ptr as *mut T) })
282}
283
284/// Project a slice of `T` from account data starting at `offset`.
285///
286/// Returns `&[T]` with `count` elements, performing bounds and alignment
287/// checks.
288#[inline]
289pub fn project_slice<T: Projectable>(
290    account: &AccountView,
291    offset: usize,
292    count: usize,
293) -> Result<&[T], ProgramError> {
294    let data_len = account.data_len();
295    let type_size = core::mem::size_of::<T>();
296    let total = count
297        .checked_mul(type_size)
298        .ok_or(ProgramError::ArithmeticOverflow)?;
299
300    if offset.checked_add(total).map_or(true, |end| end > data_len) {
301        return Err(ProgramError::AccountDataTooSmall);
302    }
303
304    let data_ptr = account.data_ptr_unchecked();
305    // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
306    let target_ptr = unsafe { data_ptr.add(offset) };
307
308    let align = core::mem::align_of::<T>();
309    if align > 1 && (target_ptr as usize) % align != 0 {
310        return Err(ProgramError::InvalidAccountData);
311    }
312
313    // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
314    Ok(unsafe { core::slice::from_raw_parts(target_ptr as *const T, count) })
315}
316
317/// Project with a Hopper standard header: skip the 10-byte header
318/// (1 disc + 1 version + 8 layout_id) and project `T` starting at
319/// byte 10. Verifies discriminator.
320///
321/// This is the most common projection pattern for Hopper accounts.
322#[inline]
323pub fn project_hopper<T: Projectable>(
324    account: &AccountView,
325    expected_disc: u8,
326) -> Result<&T, ProgramError> {
327    project::<T>(account, 10, Some(expected_disc))
328}
329
330/// Mutable version of `project_hopper`.
331///
332/// # Safety
333///
334/// Caller must ensure exclusive access to the account data.
335#[inline]
336pub unsafe fn project_hopper_mut<T: Projectable>(
337    account: &AccountView,
338    expected_disc: u8,
339) -> Result<&mut T, ProgramError> {
340    // SAFETY: This block is part of Hopper's audited zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
341    unsafe { project_mut::<T>(account, 10, Some(expected_disc)) }
342}