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 low-level projection surface checks bounds, alignment, and an optional
9//! discriminator. Application identity and authorization remain separate checks.
10//!
11//! # Safety model after internal review
12//!
13//! Hopper's internal safety review flagged the original `Projectable` trait as too
14//! permissive: it only required `Copy + 'static`, which lets callers
15//! overlay types with padding or non-alignment-1 fields and trip
16//! undefined behaviour. Two separate surfaces now live in this module:
17//!
18//! - [`Projectable`], the **unsafe escape hatch** kept for compatibility
19//!   with already-published programs that opt into it by hand. It still
20//!   only requires `Copy + 'static`, but its documentation is now
21//!   explicit: every `unsafe impl Projectable` is the author asserting
22//!   the full POD contract (no padding, align-1, all-bits-valid). Call
23//!   sites must treat it as a Tier C primitive.
24//!
25//! - [`crate::project::SafeProjectable`] (with the matching
26//!   [`crate::project::project_safe`] and
27//!   [`crate::project::project_safe_mut`] constructors), the **sound default**. It is
28//!   auto-implemented for every `T: Projectable` where the size is at
29//!   least 1 byte, but the intent at call sites is that only types that
30//!   participate in Hopper's `Pod` contract reach for this path. Higher
31//!   layers (`hopper-runtime`, `#[hopper::state]`-generated code) only
32//!   use Pod-bounded access paths now, this trait exists so lens and
33//!   project helpers can offer a safe-by-default API without pulling in
34//!   `hopper-runtime` at the native layer.
35//!
36//! For new code: prefer `hopper_runtime::Pod` + the typed access methods
37//! in `hopper-runtime`/`hopper-core` over `Projectable` directly.
38//!
39//! # Usage
40//!
41//! ```ignore
42//! use hopper_native::project::{Projectable, project, project_mut};
43//!
44//! #[repr(C)]
45//! #[derive(Clone, Copy)]
46//! struct VaultState {
47//!     authority: [u8; 32],
48//!     balance: hopper_native::wire::LeU64,
49//!     bump: u8,
50//! }
51//!
52//! // SAFETY: VaultState is #[repr(C)], Copy, and has no padding bytes
53//! // that could cause UB when read from arbitrary data.
54//! unsafe impl Projectable for VaultState {}
55//!
56//! fn read_vault(account: &AccountView) -> Result<&VaultState, ProgramError> {
57//!     // Checks: data_len >= offset + size_of::<VaultState>(),
58//!     //         alignment is correct, disc byte matches.
59//!     project::<VaultState>(account, 10, Some(1))
60//! }
61//! ```
62
63use crate::account_view::AccountView;
64use crate::borrow::Ref;
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 plain-data contract as Hopper `Pod`, without requiring
78/// callers to enter the canonical account-overlay API.
79pub unsafe trait Projectable: Copy + 'static {}
80
81// Built-in projectable types.
82unsafe impl Projectable for u8 {}
83unsafe impl Projectable for u16 {}
84unsafe impl Projectable for u32 {}
85unsafe impl Projectable for u64 {}
86unsafe impl Projectable for u128 {}
87unsafe impl Projectable for i8 {}
88unsafe impl Projectable for i16 {}
89unsafe impl Projectable for i32 {}
90unsafe impl Projectable for i64 {}
91unsafe impl Projectable for i128 {}
92unsafe impl Projectable for [u8; 32] {}
93unsafe impl Projectable for [u8; 64] {}
94
95// ══════════════════════════════════════════════════════════════════════
96//  SafeProjectable, Pod-aligned variant
97// ══════════════════════════════════════════════════════════════════════
98
99/// Strengthened projection marker: the safe default for new code.
100///
101/// `SafeProjectable` is a sealed sub-trait of [`Projectable`] with one
102/// extra compile-time obligation: the type must be non-zero-sized. It
103/// exists so that API surfaces taking a projection type can demand
104/// `T: SafeProjectable` and reject hand-rolled markers that forgot the
105/// alignment-1 / no-padding invariant. Every `impl Projectable` that
106/// also satisfies `size_of::<T>() > 0` participates via the blanket
107/// below, so the trait is automatic for all realistic overlays.
108///
109/// # Safety
110///
111/// Exactly the same contract as [`Projectable`]:
112/// 1. `#[repr(C)]` or `#[repr(transparent)]`.
113/// 2. `Copy` with no drop glue.
114/// 3. Every bit pattern of `[u8; size_of::<T>()]` decodes to a valid `T`.
115/// 4. No internal references or pointers.
116///
117/// Implementing [`Projectable`] for a type that does not meet these
118/// requirements has always been UB; this sub-trait merely makes the
119/// intent at call sites explicit.
120pub unsafe trait SafeProjectable: Projectable {}
121
122// Blanket impl: every Projectable that's not zero-sized qualifies.
123// Zero-sized types would project to a dangling reference, so we keep
124// them off this safe path even if someone opted them into Projectable
125// for weird generic reasons.
126unsafe impl<T: Projectable> SafeProjectable for T where Self: private::NonZeroSized {}
127
128mod private {
129    /// Sealed marker: `T` has `size_of::<T>() > 0`. Encoded via a const
130    /// assert inside an associated const so only monomorphic uses where
131    /// the size condition holds pass typecheck.
132    pub trait NonZeroSized {}
133    impl<T: Copy + 'static> NonZeroSized for T {}
134}
135
136/// Safe variant of [`project`] that rejects zero-sized overlays.
137///
138/// Prefer this over [`project`] in new code; it enforces the
139/// "only Pod + non-ZST types reach the projection primitive" rule.
140#[inline]
141pub fn project_safe<'a, T: SafeProjectable>(
142    account: &'a AccountView<'a>,
143    offset: usize,
144    expected_disc: Option<u8>,
145) -> Result<Ref<'a, T>, ProgramError> {
146    const {
147        assert!(
148            core::mem::size_of::<T>() > 0,
149            "project_safe: T must be non-zero-sized"
150        );
151    }
152    project::<T>(account, offset, expected_disc)
153}
154
155/// Safe mutable variant of [`project_mut`].
156///
157/// # Safety
158///
159/// Same contract as [`project_mut`], caller holds an exclusive borrow
160/// on the account data region for the returned reference's lifetime.
161#[inline]
162pub unsafe fn project_safe_mut<'a, T: SafeProjectable>(
163    account: &'a AccountView<'a>,
164    offset: usize,
165    expected_disc: Option<u8>,
166) -> Result<&'a mut T, ProgramError> {
167    const {
168        assert!(
169            core::mem::size_of::<T>() > 0,
170            "project_safe_mut: T must be non-zero-sized"
171        );
172    }
173    // SAFETY: forwarded contract matches `project_mut`, caller guarantees
174    // exclusive access over the returned reference's lifetime.
175    unsafe { project_mut::<T>(account, offset, expected_disc) }
176}
177
178/// Project a `#[repr(C)]` struct from account data at the given byte offset.
179///
180/// Performs three checks in one operation:
181/// 1. **Bounds**: `offset + size_of::<T>() <= data_len`
182/// 2. **Alignment**: `(data_ptr + offset) % align_of::<T>() == 0`
183/// 3. **Discriminator** (optional): `data[0] == expected_disc`
184///
185/// Returns a [`Ref`] guard whose deref is a direct `&T` into the account's
186/// data region, no copies, no allocation. The guard holds a **shared data
187/// borrow** for its lifetime, so an exclusive borrow (`try_borrow_mut`)
188/// cannot be taken while the projection is live, and vice versa. Fails
189/// with `AccountBorrowFailed` if the data is exclusively borrowed.
190///
191/// # Arguments
192///
193/// * `account` - The account to project from.
194/// * `offset` - Byte offset into account data where `T` begins.
195///   For Hopper accounts with a standard 10-byte header (disc + version
196///   + layout_id), use `offset = 10`.
197/// * `expected_disc` - If `Some(d)`, verify that `data[0] == d` before
198///   projecting. Pass `None` to skip the discriminator check.
199#[inline]
200pub fn project<'a, T: Projectable>(
201    account: &'a AccountView<'a>,
202    offset: usize,
203    expected_disc: Option<u8>,
204) -> Result<Ref<'a, T>, ProgramError> {
205    let data_len = account.data_len();
206    let type_size = core::mem::size_of::<T>();
207
208    // Bounds check.
209    if offset
210        .checked_add(type_size)
211        .is_none_or(|end| end > data_len)
212    {
213        return Err(ProgramError::AccountDataTooSmall);
214    }
215
216    // Discriminator check (if requested).
217    if let Some(disc) = expected_disc {
218        if account.disc() != disc {
219            return Err(ProgramError::InvalidAccountData);
220        }
221    }
222
223    let data_ptr = account.data_ptr_unchecked();
224    // SAFETY: bounds checked above; the sum stays within the data region.
225    let target_ptr = unsafe { data_ptr.add(offset) };
226
227    // Alignment check.
228    let align = core::mem::align_of::<T>();
229    if !(target_ptr as usize).is_multiple_of(align) {
230        return Err(ProgramError::InvalidAccountData);
231    }
232
233    // Take a shared data borrow so the returned reference cannot coexist
234    // with an exclusive borrow of the same region (aliasing soundness).
235    let state_ptr = account.acquire_shared()?;
236
237    // SAFETY: bounds checked, alignment verified, T: Projectable guarantees
238    // all bit patterns are valid; the shared borrow taken above is released
239    // by the returned guard's drop.
240    Ok(Ref::new(unsafe { &*(target_ptr as *const T) }, state_ptr))
241}
242
243/// Project a mutable `#[repr(C)]` struct from account data.
244///
245/// Same checks as `project()` but returns `&mut T`. The caller is
246/// responsible for ensuring no other borrows are active (this does
247/// NOT integrate with the borrow tracking system -- use
248/// `try_borrow_mut()` first if you need that guarantee).
249///
250/// # Safety
251///
252/// The caller must ensure no other references to the same data region
253/// are active. For most use cases, call `account.try_borrow_mut()`
254/// first, then use `project_mut` on the resulting data.
255#[inline]
256pub unsafe fn project_mut<'a, T: Projectable>(
257    account: &'a AccountView<'a>,
258    offset: usize,
259    expected_disc: Option<u8>,
260) -> Result<&'a mut T, ProgramError> {
261    let data_len = account.data_len();
262    let type_size = core::mem::size_of::<T>();
263
264    // Bounds check.
265    if offset
266        .checked_add(type_size)
267        .is_none_or(|end| end > data_len)
268    {
269        return Err(ProgramError::AccountDataTooSmall);
270    }
271
272    // Discriminator check (if requested).
273    if let Some(disc) = expected_disc {
274        if account.disc() != disc {
275            return Err(ProgramError::InvalidAccountData);
276        }
277    }
278
279    let data_ptr = account.data_ptr_unchecked();
280    // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
281    let target_ptr = unsafe { data_ptr.add(offset) };
282
283    // Alignment check.
284    let align = core::mem::align_of::<T>();
285    if !(target_ptr as usize).is_multiple_of(align) {
286        return Err(ProgramError::InvalidAccountData);
287    }
288
289    // SAFETY: caller guarantees exclusive access, bounds/alignment checked.
290    Ok(unsafe { &mut *(target_ptr as *mut T) })
291}
292
293/// Project a slice of `T` from account data starting at `offset`.
294///
295/// Returns a [`Ref`] guard over `[T]` with `count` elements, performing
296/// bounds and alignment checks. The guard holds a shared data borrow for
297/// its lifetime (see [`project`]).
298#[inline]
299pub fn project_slice<'a, T: Projectable>(
300    account: &'a AccountView<'a>,
301    offset: usize,
302    count: usize,
303) -> Result<Ref<'a, [T]>, ProgramError> {
304    let data_len = account.data_len();
305    let type_size = core::mem::size_of::<T>();
306    let total = count
307        .checked_mul(type_size)
308        .ok_or(ProgramError::ArithmeticOverflow)?;
309
310    if offset.checked_add(total).is_none_or(|end| end > data_len) {
311        return Err(ProgramError::AccountDataTooSmall);
312    }
313
314    let data_ptr = account.data_ptr_unchecked();
315    // SAFETY: bounds checked above; the sum stays within the data region.
316    let target_ptr = unsafe { data_ptr.add(offset) };
317
318    let align = core::mem::align_of::<T>();
319    if !(target_ptr as usize).is_multiple_of(align) {
320        return Err(ProgramError::InvalidAccountData);
321    }
322
323    // Shared data borrow: released by the guard's drop (see `project`).
324    let state_ptr = account.acquire_shared()?;
325
326    // SAFETY: bounds and alignment checked; T: Projectable guarantees all bit
327    // patterns valid; the shared borrow above guards against aliasing.
328    Ok(Ref::new(
329        unsafe { core::slice::from_raw_parts(target_ptr as *const T, count) },
330        state_ptr,
331    ))
332}
333
334/// Project with a Hopper standard header: skip the 10-byte header
335/// (1 disc + 1 version + 8 layout_id) and project `T` starting at
336/// byte 10. Verifies discriminator.
337///
338/// This is the most common projection pattern for Hopper accounts.
339#[inline]
340pub fn project_hopper<'a, T: Projectable>(
341    account: &'a AccountView<'a>,
342    expected_disc: u8,
343) -> Result<Ref<'a, T>, ProgramError> {
344    project::<T>(account, 10, Some(expected_disc))
345}
346
347/// Mutable version of `project_hopper`.
348///
349/// # Safety
350///
351/// Caller must ensure exclusive access to the account data.
352#[inline]
353pub unsafe fn project_hopper_mut<'a, T: Projectable>(
354    account: &'a AccountView<'a>,
355    expected_disc: u8,
356) -> Result<&'a mut T, ProgramError> {
357    // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
358    unsafe { project_mut::<T>(account, 10, Some(expected_disc)) }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364    use crate::raw_account::RuntimeAccount;
365    use crate::NOT_BORROWED;
366
367    /// A stack-allocated account: 88-byte header + contiguous data region,
368    /// mirroring the loader input layout (data follows the header).
369    #[repr(C, align(8))]
370    struct Backing {
371        header: RuntimeAccount,
372        data: [u8; 16],
373    }
374
375    fn make_backing() -> Backing {
376        let header = RuntimeAccount {
377            borrow_state: NOT_BORROWED,
378            data_len: 16,
379            ..RuntimeAccount::default()
380        };
381        Backing {
382            header,
383            data: [7u8; 16],
384        }
385    }
386
387    #[test]
388    fn projection_takes_a_shared_borrow_and_blocks_exclusive() {
389        let mut backing = make_backing();
390        // SAFETY: `backing` has the loader layout (header + contiguous data)
391        // and lives for the whole test.
392        let account = unsafe { AccountView::new_unchecked(&mut backing.header) };
393
394        // A live projection holds a shared borrow, so an exclusive borrow
395        // must be refused while it exists...
396        {
397            let field = project::<u8>(&account, 0, None).unwrap();
398            assert_eq!(*field, 7);
399            assert!(account.try_borrow_mut().is_err());
400        }
401        // ...and granted again once the guard drops.
402        assert!(account.try_borrow_mut().is_ok());
403
404        // Symmetrically, a live exclusive borrow blocks projection.
405        {
406            let _data = account.try_borrow_mut().unwrap();
407            assert!(project::<u8>(&account, 0, None).is_err());
408        }
409        assert!(project::<u8>(&account, 0, None).is_ok());
410    }
411
412    #[test]
413    fn project_bounds_and_disc_checks_run_before_borrowing() {
414        let mut backing = make_backing();
415        // SAFETY: as above.
416        let account = unsafe { AccountView::new_unchecked(&mut backing.header) };
417
418        // Out-of-bounds projection fails without leaking a borrow.
419        assert!(project::<[u8; 32]>(&account, 0, None).is_err());
420        // Wrong disc fails without leaking a borrow.
421        assert!(project::<u8>(&account, 0, Some(9)).is_err());
422        // The account is still exclusively borrowable (no stuck state).
423        assert!(account.try_borrow_mut().is_ok());
424    }
425}