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}