Skip to main content

hopper_runtime/
tail.rs

1//! Hybrid serialization tail for `#[hopper::state(dynamic_tail = T)]`.
2//!
3//! Closes Hopper Safety Audit innovation I5 ("Hybrid serialization").
4//! The rationale from the audit (page 14):
5//!
6//! > Lets Hopper own the fixed-layout hot path while still supporting a
7//! > dynamic tail for vectors, strings, and optional metadata.
8//!
9//! # Wire format
10//!
11//! After the layout's fixed body (offset `TYPE_OFFSET + WIRE_SIZE`), the
12//! tail is encoded as:
13//!
14//! ```text
15//! [ len: u32 LE ] [ payload: len bytes ]
16//! ```
17//!
18//! The fixed-body fast path remains fully zero-copy. code that never
19//! touches the tail pays zero overhead. Tail access is explicit
20//! (`tail_read::<T>()` / `tail_write::<T>()`), which is why the tail
21//! is **not** zero-copy: the typed representation is reconstructed on
22//! read and serialized on write.
23//!
24//! # Canonical tail encoding (`TailCodec`)
25//!
26//! `TailCodec` is a minimal Borsh-subset serializer:
27//!
28//! * integers: native little-endian
29//! * `[u8; N]`: raw bytes, fixed width
30//! * bounded byte/string payloads: program-defined length prefix + bytes
31//! * `Option<T>`: 1-byte tag (0 = None, 1 = Some) + inner payload
32//!
33//! Programs that need richer types (bounded strings, bounded vectors,
34//! custom structs) implement `TailCodec` themselves; the framework does not
35//! force a derive or pull `Vec` / `String` into the no-alloc runtime surface.
36
37use crate::error::ProgramError;
38
39/// Canonical serializer for dynamic-tail payloads.
40///
41/// Implementations encode into a caller-provided buffer and decode
42/// from a caller-provided slice, returning the byte count consumed
43/// in both directions. Byte counts drive the length-prefix handling
44/// inside `#[hopper::state]`'s generated tail accessors. the
45/// encoding must be deterministic and bidirectional.
46pub trait TailCodec: Sized {
47    /// Upper bound on the encoded size. Used by generated helpers to
48    /// verify the account has enough room before invoking `encode`.
49    /// Implementors should pick the smallest valid bound. Hopper
50    /// uses this to pre-size reallocs.
51    const MAX_ENCODED_LEN: usize;
52
53    /// Serialize `self` into `out`. Returns the number of bytes
54    /// written (always `<= MAX_ENCODED_LEN`). Fails with
55    /// `AccountDataTooSmall` when `out.len() < encoded_len`.
56    fn encode(&self, out: &mut [u8]) -> Result<usize, ProgramError>;
57
58    /// Deserialize from `input`. Returns `(value, bytes_consumed)`.
59    /// Fails with `InvalidAccountData` on malformed encoding.
60    fn decode(input: &[u8]) -> Result<(Self, usize), ProgramError>;
61}
62
63/// Element type accepted by `#[tail(vec<T, N>)]` in `#[hopper::dynamic_account]`.
64///
65/// A tail element must have deterministic Hopper tail encoding, be cheap to copy
66/// into the fixed-capacity backing array, have a default empty slot value, and be
67/// comparable for generated `push_unique_*` / `remove_*` helpers.
68pub trait TailElement: TailCodec + Copy + Default + PartialEq {}
69
70impl<T> TailElement for T where T: TailCodec + Copy + Default + PartialEq {}
71
72// ── Primitive impls (little-endian, fixed width) ────────────────────
73
74impl TailCodec for u8 {
75    const MAX_ENCODED_LEN: usize = 1;
76    #[inline]
77    fn encode(&self, out: &mut [u8]) -> Result<usize, ProgramError> {
78        if out.is_empty() {
79            return Err(ProgramError::AccountDataTooSmall);
80        }
81        out[0] = *self;
82        Ok(1)
83    }
84    #[inline]
85    fn decode(input: &[u8]) -> Result<(Self, usize), ProgramError> {
86        input
87            .first()
88            .copied()
89            .map(|b| (b, 1))
90            .ok_or(ProgramError::InvalidAccountData)
91    }
92}
93
94macro_rules! tail_codec_int {
95    ( $( $ty:ty : $n:expr ),+ $(,)? ) => {
96        $(
97            impl TailCodec for $ty {
98                const MAX_ENCODED_LEN: usize = $n;
99                #[inline]
100                fn encode(&self, out: &mut [u8]) -> Result<usize, ProgramError> {
101                    if out.len() < $n {
102                        return Err(ProgramError::AccountDataTooSmall);
103                    }
104                    out[..$n].copy_from_slice(&self.to_le_bytes());
105                    Ok($n)
106                }
107                #[inline]
108                fn decode(input: &[u8]) -> Result<(Self, usize), ProgramError> {
109                    if input.len() < $n {
110                        return Err(ProgramError::InvalidAccountData);
111                    }
112                    let mut bytes = [0u8; $n];
113                    bytes.copy_from_slice(&input[..$n]);
114                    Ok((Self::from_le_bytes(bytes), $n))
115                }
116            }
117        )+
118    };
119}
120
121tail_codec_int! {
122    u16: 2, u32: 4, u64: 8, u128: 16,
123    i16: 2, i32: 4, i64: 8, i128: 16,
124}
125
126// `bool` as 1 byte (0 = false, 1 = true; anything else rejected).
127impl TailCodec for bool {
128    const MAX_ENCODED_LEN: usize = 1;
129    #[inline]
130    fn encode(&self, out: &mut [u8]) -> Result<usize, ProgramError> {
131        if out.is_empty() {
132            return Err(ProgramError::AccountDataTooSmall);
133        }
134        out[0] = if *self { 1 } else { 0 };
135        Ok(1)
136    }
137    #[inline]
138    fn decode(input: &[u8]) -> Result<(Self, usize), ProgramError> {
139        match input.first().copied() {
140            Some(0) => Ok((false, 1)),
141            Some(1) => Ok((true, 1)),
142            _ => Err(ProgramError::InvalidAccountData),
143        }
144    }
145}
146
147// `[u8; N]`. raw fixed-width bytes.
148impl<const N: usize> TailCodec for [u8; N] {
149    const MAX_ENCODED_LEN: usize = N;
150    #[inline]
151    fn encode(&self, out: &mut [u8]) -> Result<usize, ProgramError> {
152        if out.len() < N {
153            return Err(ProgramError::AccountDataTooSmall);
154        }
155        out[..N].copy_from_slice(self);
156        Ok(N)
157    }
158    #[inline]
159    fn decode(input: &[u8]) -> Result<(Self, usize), ProgramError> {
160        if input.len() < N {
161            return Err(ProgramError::InvalidAccountData);
162        }
163        let mut out = [0u8; N];
164        out.copy_from_slice(&input[..N]);
165        Ok((out, N))
166    }
167}
168
169// `Option<T>`. 1-byte tag + inner payload when present.
170impl<T: TailCodec> TailCodec for Option<T> {
171    const MAX_ENCODED_LEN: usize = 1 + T::MAX_ENCODED_LEN;
172    #[inline]
173    fn encode(&self, out: &mut [u8]) -> Result<usize, ProgramError> {
174        if out.is_empty() {
175            return Err(ProgramError::AccountDataTooSmall);
176        }
177        match self {
178            None => {
179                out[0] = 0;
180                Ok(1)
181            }
182            Some(inner) => {
183                out[0] = 1;
184                let written = inner.encode(&mut out[1..])?;
185                Ok(1 + written)
186            }
187        }
188    }
189    #[inline]
190    fn decode(input: &[u8]) -> Result<(Self, usize), ProgramError> {
191        match input.first().copied() {
192            Some(0) => Ok((None, 1)),
193            Some(1) => {
194                let (inner, n) = T::decode(&input[1..])?;
195                Ok((Some(inner), 1 + n))
196            }
197            _ => Err(ProgramError::InvalidAccountData),
198        }
199    }
200}
201
202// -- Bounded dynamic-tail helpers ------------------------------------------
203
204/// Bounded UTF-8 string for Hopper dynamic tails.
205///
206/// This is the common migration target for bounded string account metadata.
207/// It keeps a fixed `[u8; N]` backing buffer, carries a small length prefix on
208/// the tail wire, and validates UTF-8 when read as `&str`.
209#[derive(Clone, Copy, Eq, PartialEq)]
210pub struct BoundedString<const N: usize> {
211    len: u16,
212    bytes: [u8; N],
213}
214
215impl<const N: usize> BoundedString<N> {
216    /// Construct an empty bounded string.
217    #[inline]
218    pub const fn empty() -> Self {
219        Self {
220            len: 0,
221            bytes: [0u8; N],
222        }
223    }
224
225    /// Construct from UTF-8 bytes, rejecting values longer than `N`.
226    #[inline]
227    pub fn from_str(value: &str) -> Result<Self, ProgramError> {
228        Self::from_bytes(value.as_bytes())
229    }
230
231    /// Construct from bytes, rejecting values longer than `N`.
232    #[inline]
233    pub fn from_bytes(value: &[u8]) -> Result<Self, ProgramError> {
234        if value.len() > N || value.len() > u16::MAX as usize {
235            return Err(ProgramError::InvalidInstructionData);
236        }
237        let mut out = Self::empty();
238        out.bytes[..value.len()].copy_from_slice(value);
239        out.len = value.len() as u16;
240        Ok(out)
241    }
242
243    /// Replace the contents in place.
244    #[inline]
245    pub fn set_str(&mut self, value: &str) -> Result<(), ProgramError> {
246        self.set_bytes(value.as_bytes())
247    }
248
249    /// Replace the contents in place.
250    #[inline]
251    pub fn set_bytes(&mut self, value: &[u8]) -> Result<(), ProgramError> {
252        if value.len() > N || value.len() > u16::MAX as usize {
253            return Err(ProgramError::InvalidInstructionData);
254        }
255        self.bytes = [0u8; N];
256        self.bytes[..value.len()].copy_from_slice(value);
257        self.len = value.len() as u16;
258        Ok(())
259    }
260
261    /// Clear the string without changing its capacity.
262    #[inline]
263    pub fn clear(&mut self) {
264        self.bytes = [0u8; N];
265        self.len = 0;
266    }
267
268    /// Return the initialized bytes.
269    #[inline(always)]
270    pub fn as_bytes(&self) -> &[u8] {
271        &self.bytes[..self.len as usize]
272    }
273
274    /// Return the initialized bytes as UTF-8.
275    #[inline]
276    pub fn as_str(&self) -> Result<&str, ProgramError> {
277        core::str::from_utf8(self.as_bytes()).map_err(|_| ProgramError::InvalidAccountData)
278    }
279
280    /// Number of initialized bytes.
281    #[inline(always)]
282    pub const fn len(&self) -> usize {
283        self.len as usize
284    }
285
286    /// Maximum byte capacity.
287    #[inline(always)]
288    pub const fn capacity(&self) -> usize {
289        N
290    }
291
292    /// Remaining byte capacity.
293    #[inline(always)]
294    pub const fn remaining_capacity(&self) -> usize {
295        N - self.len as usize
296    }
297
298    /// Whether the string has reached its maximum byte capacity.
299    #[inline(always)]
300    pub const fn is_full(&self) -> bool {
301        self.len as usize == N
302    }
303
304    /// Whether the string is empty.
305    #[inline(always)]
306    pub const fn is_empty(&self) -> bool {
307        self.len == 0
308    }
309}
310
311impl<const N: usize> Default for BoundedString<N> {
312    #[inline]
313    fn default() -> Self {
314        Self::empty()
315    }
316}
317
318impl<const N: usize> TailCodec for BoundedString<N> {
319    const MAX_ENCODED_LEN: usize = 2 + N;
320
321    #[inline]
322    fn encode(&self, out: &mut [u8]) -> Result<usize, ProgramError> {
323        let len = self.len as usize;
324        if len > N || out.len() < 2 + len {
325            return Err(ProgramError::AccountDataTooSmall);
326        }
327        out[..2].copy_from_slice(&self.len.to_le_bytes());
328        out[2..2 + len].copy_from_slice(&self.bytes[..len]);
329        Ok(2 + len)
330    }
331
332    #[inline]
333    fn decode(input: &[u8]) -> Result<(Self, usize), ProgramError> {
334        if input.len() < 2 {
335            return Err(ProgramError::InvalidAccountData);
336        }
337        let len = u16::from_le_bytes([input[0], input[1]]) as usize;
338        if len > N || input.len() < 2 + len {
339            return Err(ProgramError::InvalidAccountData);
340        }
341        let mut out = Self::empty();
342        out.len = len as u16;
343        out.bytes[..len].copy_from_slice(&input[2..2 + len]);
344        Ok((out, 2 + len))
345    }
346}
347
348/// Bounded dynamic vector for Hopper dynamic tails.
349///
350/// This is the common migration target for bounded vector account metadata.
351/// Elements use `TailCodec`, so the vector can carry wire integers, addresses,
352/// or small custom structs declared with `hopper_dynamic_tail!`.
353#[derive(Clone, Copy, Eq, PartialEq)]
354pub struct BoundedVec<T, const N: usize>
355where
356    T: TailCodec + Copy + Default,
357{
358    len: u16,
359    items: [T; N],
360}
361
362impl<T, const N: usize> BoundedVec<T, N>
363where
364    T: TailCodec + Copy + Default,
365{
366    /// Construct an empty bounded vector.
367    #[inline]
368    pub fn empty() -> Self {
369        Self {
370            len: 0,
371            items: [T::default(); N],
372        }
373    }
374
375    /// Construct from a slice, rejecting values longer than `N`.
376    #[inline]
377    pub fn from_slice(values: &[T]) -> Result<Self, ProgramError> {
378        if values.len() > N || values.len() > u16::MAX as usize {
379            return Err(ProgramError::InvalidInstructionData);
380        }
381        let mut out = Self::empty();
382        out.items[..values.len()].copy_from_slice(values);
383        out.len = values.len() as u16;
384        Ok(out)
385    }
386
387    /// Push one item into the bounded vector.
388    #[inline]
389    pub fn push(&mut self, item: T) -> Result<(), ProgramError> {
390        let len = self.len as usize;
391        if len >= N || len >= u16::MAX as usize {
392            return Err(ProgramError::AccountDataTooSmall);
393        }
394        self.items[len] = item;
395        self.len += 1;
396        Ok(())
397    }
398
399    /// Pop the last initialized item, if present.
400    #[inline]
401    pub fn pop(&mut self) -> Option<T> {
402        let len = self.len as usize;
403        if len == 0 {
404            return None;
405        }
406        let new_len = len - 1;
407        let item = self.items[new_len];
408        self.items[new_len] = T::default();
409        self.len = new_len as u16;
410        Some(item)
411    }
412
413    /// Clear all initialized items without changing capacity.
414    #[inline]
415    pub fn clear(&mut self) {
416        let len = self.len as usize;
417        let mut i = 0;
418        while i < len {
419            self.items[i] = T::default();
420            i += 1;
421        }
422        self.len = 0;
423    }
424
425    /// Return the initialized items.
426    #[inline(always)]
427    pub fn as_slice(&self) -> &[T] {
428        &self.items[..self.len as usize]
429    }
430
431    /// Return the initialized items mutably.
432    #[inline(always)]
433    pub fn as_mut_slice(&mut self) -> &mut [T] {
434        &mut self.items[..self.len as usize]
435    }
436
437    /// Number of initialized items.
438    #[inline(always)]
439    pub const fn len(&self) -> usize {
440        self.len as usize
441    }
442
443    /// Maximum number of items.
444    #[inline(always)]
445    pub const fn capacity(&self) -> usize {
446        N
447    }
448
449    /// Remaining element capacity.
450    #[inline(always)]
451    pub const fn remaining_capacity(&self) -> usize {
452        N - self.len as usize
453    }
454
455    /// Whether the vector has reached its maximum element capacity.
456    #[inline(always)]
457    pub const fn is_full(&self) -> bool {
458        self.len as usize == N
459    }
460
461    /// Whether the vector is empty.
462    #[inline(always)]
463    pub const fn is_empty(&self) -> bool {
464        self.len == 0
465    }
466}
467
468impl<T, const N: usize> BoundedVec<T, N>
469where
470    T: TailCodec + Copy + Default + PartialEq,
471{
472    /// Return true when the initialized items contain `item`.
473    #[inline]
474    pub fn contains(&self, item: &T) -> bool {
475        self.as_slice().iter().any(|candidate| candidate == item)
476    }
477
478    /// Push `item` only if it is not already initialized.
479    ///
480    /// Returns `Ok(true)` when an item was inserted and `Ok(false)` when it
481    /// was already present.
482    #[inline]
483    pub fn push_unique(&mut self, item: T) -> Result<bool, ProgramError> {
484        if self.contains(&item) {
485            return Ok(false);
486        }
487        self.push(item)?;
488        Ok(true)
489    }
490
491    /// Remove the first matching item, preserving order.
492    ///
493    /// Returns `true` when an item was removed.
494    #[inline]
495    pub fn remove_first(&mut self, item: &T) -> bool {
496        let len = self.len as usize;
497        let mut found = None;
498        let mut i = 0;
499        while i < len {
500            if &self.items[i] == item {
501                found = Some(i);
502                break;
503            }
504            i += 1;
505        }
506        let Some(index) = found else {
507            return false;
508        };
509        let mut j = index;
510        while j + 1 < len {
511            self.items[j] = self.items[j + 1];
512            j += 1;
513        }
514        self.items[len - 1] = T::default();
515        self.len = (len - 1) as u16;
516        true
517    }
518}
519
520/// Short alias for bounded UTF-8 strings in dynamic tails.
521pub type HopperString<const N: usize> = BoundedString<N>;
522
523/// Short alias for bounded vectors in dynamic tails.
524pub type HopperVec<T, const N: usize> = BoundedVec<T, N>;
525
526impl<T, const N: usize> Default for BoundedVec<T, N>
527where
528    T: TailCodec + Copy + Default,
529{
530    #[inline]
531    fn default() -> Self {
532        Self::empty()
533    }
534}
535
536impl<T, const N: usize> TailCodec for BoundedVec<T, N>
537where
538    T: TailCodec + Copy + Default,
539{
540    const MAX_ENCODED_LEN: usize = 2 + (N * T::MAX_ENCODED_LEN);
541
542    #[inline]
543    fn encode(&self, out: &mut [u8]) -> Result<usize, ProgramError> {
544        let len = self.len as usize;
545        if len > N || out.len() < 2 {
546            return Err(ProgramError::AccountDataTooSmall);
547        }
548        out[..2].copy_from_slice(&self.len.to_le_bytes());
549        let mut cursor = 2;
550        for item in self.as_slice() {
551            let written = item.encode(&mut out[cursor..])?;
552            cursor = cursor
553                .checked_add(written)
554                .ok_or(ProgramError::AccountDataTooSmall)?;
555        }
556        Ok(cursor)
557    }
558
559    #[inline]
560    fn decode(input: &[u8]) -> Result<(Self, usize), ProgramError> {
561        if input.len() < 2 {
562            return Err(ProgramError::InvalidAccountData);
563        }
564        let len = u16::from_le_bytes([input[0], input[1]]) as usize;
565        if len > N {
566            return Err(ProgramError::InvalidAccountData);
567        }
568        let mut out = Self::empty();
569        let mut cursor = 2;
570        let mut i = 0;
571        while i < len {
572            let (item, consumed) = T::decode(&input[cursor..])?;
573            out.items[i] = item;
574            cursor = cursor
575                .checked_add(consumed)
576                .ok_or(ProgramError::InvalidAccountData)?;
577            i += 1;
578        }
579        out.len = len as u16;
580        Ok((out, cursor))
581    }
582}
583
584impl TailCodec for crate::address::Address {
585    const MAX_ENCODED_LEN: usize = 32;
586
587    #[inline]
588    fn encode(&self, out: &mut [u8]) -> Result<usize, ProgramError> {
589        if out.len() < 32 {
590            return Err(ProgramError::AccountDataTooSmall);
591        }
592        out[..32].copy_from_slice(self.as_array());
593        Ok(32)
594    }
595
596    #[inline]
597    fn decode(input: &[u8]) -> Result<(Self, usize), ProgramError> {
598        if input.len() < 32 {
599            return Err(ProgramError::InvalidAccountData);
600        }
601        let mut bytes = [0u8; 32];
602        bytes.copy_from_slice(&input[..32]);
603        Ok((crate::address::Address::new(bytes), 32))
604    }
605}
606
607// ── Framework helpers used by `#[hopper::state(dynamic_tail = T)]` ──
608
609/// Read the tail's u32-LE length prefix.
610///
611/// `body_end` is the byte offset immediately after the layout's fixed
612/// body (i.e. `TYPE_OFFSET + WIRE_SIZE` for a layout with no header
613/// beyond the 16-byte Hopper prefix, otherwise `HEADER_LEN +
614/// WIRE_SIZE`). Returns `AccountDataTooSmall` if the account has
615/// fewer than 4 tail bytes available.
616#[inline]
617pub fn read_tail_len(data: &[u8], body_end: usize) -> Result<u32, ProgramError> {
618    let end = body_end
619        .checked_add(4)
620        .ok_or(ProgramError::AccountDataTooSmall)?;
621    if data.len() < end {
622        return Err(ProgramError::AccountDataTooSmall);
623    }
624    let mut bytes = [0u8; 4];
625    bytes.copy_from_slice(&data[body_end..end]);
626    Ok(u32::from_le_bytes(bytes))
627}
628
629/// Return a slice referencing just the tail payload bytes (excluding
630/// the 4-byte length prefix). Length-bounded by the u32 prefix.
631#[inline]
632pub fn tail_payload(data: &[u8], body_end: usize) -> Result<&[u8], ProgramError> {
633    let len = read_tail_len(data, body_end)? as usize;
634    let start = body_end + 4;
635    let end = start
636        .checked_add(len)
637        .ok_or(ProgramError::InvalidAccountData)?;
638    if data.len() < end {
639        return Err(ProgramError::InvalidAccountData);
640    }
641    Ok(&data[start..end])
642}
643
644/// Return the account bytes available after the tail length prefix.
645///
646/// This is useful before a grow/realloc path: if the encoded payload to write
647/// is larger than this value, the caller must resize the account before
648/// calling `write_tail`.
649#[inline]
650pub fn tail_capacity(data: &[u8], body_end: usize) -> Result<usize, ProgramError> {
651    let start = body_end
652        .checked_add(4)
653        .ok_or(ProgramError::AccountDataTooSmall)?;
654    if data.len() < start {
655        return Err(ProgramError::AccountDataTooSmall);
656    }
657    Ok(data.len() - start)
658}
659
660/// Borrow one bounded UTF-8 string from a compact dynamic-tail payload.
661///
662/// The returned `usize` is the number of bytes consumed from `input`, so a
663/// generated view can walk subsequent compact-tail fields without decoding the
664/// whole tail into an owned value.
665#[inline]
666pub fn borrow_bounded_str<const N: usize>(input: &[u8]) -> Result<(&str, usize), ProgramError> {
667    if input.len() < 2 {
668        return Err(ProgramError::InvalidAccountData);
669    }
670    let len = u16::from_le_bytes([input[0], input[1]]) as usize;
671    if len > N || input.len() < 2 + len {
672        return Err(ProgramError::InvalidAccountData);
673    }
674    let bytes = &input[2..2 + len];
675    let value = core::str::from_utf8(bytes).map_err(|_| ProgramError::InvalidAccountData)?;
676    Ok((value, 2 + len))
677}
678
679/// Borrow one bounded address vector from a compact dynamic-tail payload.
680///
681/// This is the zero-copy read path for the common multisig/authority-list case
682/// that Quasar represents as `Vec<'a, Address, N>`. `Address` is
683/// `repr(transparent)` over `[u8; 32]` and alignment-1, so the slice cast is
684/// layout-safe after the length and capacity checks below.
685#[inline]
686pub fn borrow_address_slice<const N: usize>(
687    input: &[u8],
688) -> Result<(&[crate::address::Address], usize), ProgramError> {
689    if input.len() < 2 {
690        return Err(ProgramError::InvalidAccountData);
691    }
692    let len = u16::from_le_bytes([input[0], input[1]]) as usize;
693    if len > N {
694        return Err(ProgramError::InvalidAccountData);
695    }
696    let byte_len = len
697        .checked_mul(32)
698        .ok_or(ProgramError::InvalidAccountData)?;
699    let end = 2usize
700        .checked_add(byte_len)
701        .ok_or(ProgramError::InvalidAccountData)?;
702    if input.len() < end {
703        return Err(ProgramError::InvalidAccountData);
704    }
705    let bytes = &input[2..end];
706    let ptr = bytes.as_ptr() as *const crate::address::Address;
707    // SAFETY: Address has alignment 1 and is transparent over [u8; 32]. The
708    // byte range length is exactly len * 32, checked above.
709    let values = unsafe { core::slice::from_raw_parts(ptr, len) };
710    Ok((values, end))
711}
712
713/// Decode the tail as `T: TailCodec`, checking that the encoded length
714/// exactly matches the u32 prefix. Extra bytes beyond `T`'s decode
715/// are a malformed-encoding signal.
716#[inline]
717pub fn read_tail<T: TailCodec>(data: &[u8], body_end: usize) -> Result<T, ProgramError> {
718    let payload = tail_payload(data, body_end)?;
719    let (value, consumed) = T::decode(payload)?;
720    if consumed != payload.len() {
721        return Err(ProgramError::InvalidAccountData);
722    }
723    Ok(value)
724}
725
726/// Encode `tail` into the account's tail slot, rewriting the u32
727/// length prefix. Returns `AccountDataTooSmall` when the existing
728/// account byte buffer can't fit the encoded payload. in that case
729/// the caller should `realloc` first.
730#[inline]
731pub fn write_tail<T: TailCodec>(
732    data: &mut [u8],
733    body_end: usize,
734    tail: &T,
735) -> Result<usize, ProgramError> {
736    let prefix_end = body_end
737        .checked_add(4)
738        .ok_or(ProgramError::AccountDataTooSmall)?;
739    if data.len() < prefix_end {
740        return Err(ProgramError::AccountDataTooSmall);
741    }
742    let written = tail.encode(&mut data[prefix_end..])?;
743    if written > u32::MAX as usize {
744        return Err(ProgramError::InvalidAccountData);
745    }
746    data[body_end..prefix_end].copy_from_slice(&(written as u32).to_le_bytes());
747    Ok(written)
748}
749
750#[cfg(test)]
751mod tests {
752    use super::*;
753
754    #[test]
755    fn u32_roundtrip() {
756        let mut buf = [0u8; 8];
757        let n = 0xDEAD_BEEFu32.encode(&mut buf).unwrap();
758        assert_eq!(n, 4);
759        let (back, consumed) = u32::decode(&buf).unwrap();
760        assert_eq!(consumed, 4);
761        assert_eq!(back, 0xDEAD_BEEF);
762    }
763
764    #[test]
765    fn u64_roundtrip() {
766        let mut buf = [0u8; 8];
767        0x0123_4567_89AB_CDEFu64.encode(&mut buf).unwrap();
768        let (back, _) = u64::decode(&buf).unwrap();
769        assert_eq!(back, 0x0123_4567_89AB_CDEF);
770    }
771
772    #[test]
773    fn bool_encode_decode() {
774        let mut buf = [0u8; 1];
775        true.encode(&mut buf).unwrap();
776        assert_eq!(buf[0], 1);
777        assert_eq!(bool::decode(&buf).unwrap(), (true, 1));
778        false.encode(&mut buf).unwrap();
779        assert_eq!(buf[0], 0);
780        assert_eq!(bool::decode(&buf).unwrap(), (false, 1));
781    }
782
783    #[test]
784    fn bool_rejects_garbage() {
785        let buf = [2u8];
786        assert!(bool::decode(&buf).is_err());
787    }
788
789    #[test]
790    fn byte_array_roundtrip() {
791        let src: [u8; 8] = *b"HOPPER!!";
792        let mut buf = [0u8; 16];
793        let n = src.encode(&mut buf).unwrap();
794        assert_eq!(n, 8);
795        let (back, consumed) = <[u8; 8]>::decode(&buf).unwrap();
796        assert_eq!(consumed, 8);
797        assert_eq!(back, src);
798    }
799
800    #[test]
801    fn option_none_encodes_to_one_byte() {
802        let mut buf = [0u8; 16];
803        let n = Option::<u64>::None.encode(&mut buf).unwrap();
804        assert_eq!(n, 1);
805        assert_eq!(buf[0], 0);
806        let (back, c) = <Option<u64>>::decode(&buf).unwrap();
807        assert_eq!(back, None);
808        assert_eq!(c, 1);
809    }
810
811    #[test]
812    fn option_some_includes_inner_payload() {
813        let mut buf = [0u8; 16];
814        let n = Option::<u64>::Some(0xAAAA_BBBB_CCCC_DDDD)
815            .encode(&mut buf)
816            .unwrap();
817        assert_eq!(n, 9);
818        assert_eq!(buf[0], 1);
819        let (back, c) = <Option<u64>>::decode(&buf).unwrap();
820        assert_eq!(back, Some(0xAAAA_BBBB_CCCC_DDDD));
821        assert_eq!(c, 9);
822    }
823
824    #[test]
825    fn option_rejects_invalid_tag() {
826        let buf = [7u8, 0, 0, 0, 0, 0, 0, 0, 0];
827        assert!(<Option<u64>>::decode(&buf).is_err());
828    }
829
830    #[test]
831    fn tail_length_prefix_roundtrip() {
832        // Simulate an account body: 16-byte "header" + 8-byte body +
833        // 4-byte length prefix + tail bytes. body_end = 24.
834        let mut data = [0u8; 64];
835        let body_end = 24usize;
836        let tail_value: u64 = 0x1234_5678_9ABC_DEF0;
837        let written = write_tail(&mut data, body_end, &tail_value).unwrap();
838        assert_eq!(written, 8);
839        let read_len = read_tail_len(&data, body_end).unwrap();
840        assert_eq!(read_len, 8);
841        let back: u64 = read_tail::<u64>(&data, body_end).unwrap();
842        assert_eq!(back, tail_value);
843    }
844
845    #[test]
846    fn tail_decode_rejects_excess_payload() {
847        // If the tail encodes as 4 bytes but the length prefix claims
848        // 8, the decode must refuse rather than silently succeed.
849        let mut data = [0u8; 32];
850        // body_end = 16; prefix says 8 bytes; payload is u32 (4 bytes) +
851        // garbage (4 bytes). Decoding as u32 leaves 4 bytes unconsumed
852        // which is caught by `read_tail`.
853        let body_end = 16usize;
854        data[body_end..body_end + 4].copy_from_slice(&8u32.to_le_bytes());
855        // Fill payload with something that decodes as u32=0x11223344
856        // and then trailing garbage.
857        data[body_end + 4..body_end + 8].copy_from_slice(&0x1122_3344u32.to_le_bytes());
858        data[body_end + 8..body_end + 12].copy_from_slice(&0xFFu32.to_le_bytes());
859        // u32 decodes 4 bytes but prefix claims 8. expect error.
860        let result = read_tail::<u32>(&data, body_end);
861        assert!(result.is_err());
862    }
863
864    #[test]
865    fn tail_bounds_check_on_short_buffer() {
866        let data = [0u8; 10];
867        assert!(read_tail_len(&data, 16).is_err());
868        assert!(tail_payload(&data, 16).is_err());
869    }
870
871    #[test]
872    fn max_encoded_len_matches_actual_encode_size() {
873        let mut buf = [0u8; 32];
874        assert_eq!(0u32.encode(&mut buf).unwrap(), u32::MAX_ENCODED_LEN);
875        assert_eq!(0u64.encode(&mut buf).unwrap(), u64::MAX_ENCODED_LEN);
876        assert_eq!(true.encode(&mut buf).unwrap(), bool::MAX_ENCODED_LEN);
877        assert_eq!(
878            [0u8; 7].encode(&mut buf).unwrap(),
879            <[u8; 7]>::MAX_ENCODED_LEN
880        );
881        assert_eq!(Option::<u32>::None.encode(&mut buf).unwrap(), 1);
882        assert_eq!(
883            Option::<u32>::Some(0).encode(&mut buf).unwrap(),
884            <Option<u32>>::MAX_ENCODED_LEN
885        );
886    }
887
888    #[test]
889    fn bounded_string_roundtrip() {
890        let label = BoundedString::<32>::from_str("multisig").unwrap();
891        let mut buf = [0u8; BoundedString::<32>::MAX_ENCODED_LEN];
892        let written = label.encode(&mut buf).unwrap();
893        assert_eq!(written, 10);
894        let (back, consumed) = BoundedString::<32>::decode(&buf).unwrap();
895        assert_eq!(consumed, written);
896        assert_eq!(back.as_str().unwrap(), "multisig");
897    }
898
899    #[test]
900    fn bounded_string_capacity_helpers() {
901        let mut label = HopperString::<8>::from_str("ops").unwrap();
902        assert_eq!(label.remaining_capacity(), 5);
903        assert!(!label.is_full());
904        label.set_str("12345678").unwrap();
905        assert!(label.is_full());
906        label.clear();
907        assert!(label.is_empty());
908        assert_eq!(label.as_bytes(), b"");
909    }
910
911    #[test]
912    fn bounded_vec_roundtrip() {
913        let mut vec = BoundedVec::<u64, 4>::empty();
914        vec.push(7).unwrap();
915        vec.push(9).unwrap();
916        let mut buf = [0u8; BoundedVec::<u64, 4>::MAX_ENCODED_LEN];
917        let written = vec.encode(&mut buf).unwrap();
918        assert_eq!(written, 18);
919        let (back, consumed) = BoundedVec::<u64, 4>::decode(&buf).unwrap();
920        assert_eq!(consumed, written);
921        assert_eq!(back.as_slice(), &[7, 9]);
922    }
923
924    #[test]
925    fn bounded_vec_set_helpers_preserve_order() {
926        let mut vec = HopperVec::<u64, 4>::empty();
927        assert_eq!(vec.remaining_capacity(), 4);
928        assert_eq!(vec.push_unique(7).unwrap(), true);
929        assert_eq!(vec.push_unique(7).unwrap(), false);
930        vec.push(9).unwrap();
931        vec.push(11).unwrap();
932        assert!(vec.contains(&9));
933        assert!(vec.remove_first(&9));
934        assert_eq!(vec.as_slice(), &[7, 11]);
935        assert_eq!(vec.pop(), Some(11));
936        assert_eq!(vec.as_slice(), &[7]);
937        vec.clear();
938        assert!(vec.is_empty());
939    }
940}
941
942#[cfg(kani)]
943mod kani_proofs {
944    use super::*;
945
946    #[kani::proof]
947    fn bounded_string_decode_never_exceeds_capacity() {
948        let len: u16 = kani::any();
949        let mut input = [0u8; BoundedString::<4>::MAX_ENCODED_LEN];
950        input[..2].copy_from_slice(&len.to_le_bytes());
951
952        let result = BoundedString::<4>::decode(&input);
953        if len as usize > 4 {
954            assert!(result.is_err());
955        } else {
956            let (decoded, consumed) = result.unwrap();
957            assert!(decoded.len() <= decoded.capacity());
958            assert_eq!(consumed, 2 + decoded.len());
959        }
960    }
961
962    #[kani::proof]
963    fn bounded_vec_mutators_preserve_capacity() {
964        let values: [u8; 5] = kani::any();
965        let mut vec = BoundedVec::<u8, 4>::empty();
966
967        let _ = vec.push(values[0]);
968        let _ = vec.push(values[1]);
969        let _ = vec.push(values[2]);
970        let _ = vec.push(values[3]);
971        let fifth = vec.push(values[4]);
972
973        assert!(vec.len() <= vec.capacity());
974        assert!(fifth.is_err());
975        let _ = vec.pop();
976        assert!(vec.len() <= vec.capacity());
977        vec.clear();
978        assert_eq!(vec.len(), 0);
979    }
980
981    #[kani::proof]
982    fn tail_payload_bounds_checks_arbitrary_prefixes() {
983        let data: [u8; 16] = kani::any();
984        let body_end: usize = kani::any();
985        kani::assume(body_end < data.len());
986
987        let result = tail_payload(&data, body_end);
988        if let Ok(payload) = result {
989            assert!(payload.len() <= data.len());
990        }
991    }
992}