1use ifx_core::layout::{decode_typed, TypedValue};
4use solana_sdk::pubkey::Pubkey;
5
6use crate::constants::ACCOUNT_DISC_FRAME;
7use crate::error::ScratchError;
8use crate::typed::ScratchValue;
9
10const OFF_AUTHORITY: usize = 1;
11const OFF_CURSOR: usize = 33;
12const OFF_INDEX_COUNT: usize = 37;
13const OFF_INDEX_CAP: usize = 39;
14const OFF_GENERATION: usize = 41;
15const OFF_PAYLOAD_AT_LEN: usize = 49;
16const OFF_PAYLOAD_AT: usize = 53;
17
18#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct DecodedFrame {
21 pub authority: Pubkey,
22 pub cursor: u32,
23 pub index_count: u16,
24 pub index_cap: u16,
25 pub generation: u64,
26 pub payload_at: Vec<u16>,
27 pub tape: Vec<u8>,
28}
29
30impl DecodedFrame {
31 pub fn read_u64(&self, sv: &ScratchValue) -> Result<u64, ScratchError> {
32 match self.read_value(sv)? {
33 TypedValue::U64(v) => Ok(v),
34 other => Err(ScratchError::FrameRead(format!(
35 "expected u64 at index {}, got {:?}",
36 sv.index, other
37 ))),
38 }
39 }
40
41 pub fn read_value(&self, sv: &ScratchValue) -> Result<TypedValue, ScratchError> {
42 let ty = sv.ty;
43 let off = self.payload_offset(sv.index)?;
44 let end = off as usize + ty.size();
45 if end > self.tape.len() {
46 return Err(ScratchError::FrameRead(format!(
47 "read past frame tape ({end} > {}) at byte {off}",
48 self.tape.len()
49 )));
50 }
51 decode_typed(ty, &self.tape[off as usize..end])
52 .map_err(|e| ScratchError::FrameRead(format!("decode payload: {e:?}")))
53 }
54
55 fn payload_offset(&self, index: u8) -> Result<u16, ScratchError> {
56 if index as usize >= self.index_count as usize {
57 return Err(ScratchError::FrameRead(format!(
58 "binding index {index} out of range (index_count={})",
59 self.index_count
60 )));
61 }
62 self.payload_at
63 .get(index as usize)
64 .copied()
65 .ok_or_else(|| ScratchError::FrameRead(format!("missing payload_at[{index}]")))
66 }
67}
68
69pub fn decode_frame_account(data: &[u8]) -> Result<DecodedFrame, ScratchError> {
71 if data.len() < OFF_PAYLOAD_AT + 4 + 4 {
72 return Err(ScratchError::FrameDecode("frame account data too short".into()));
73 }
74 if data[0] != ACCOUNT_DISC_FRAME {
75 return Err(ScratchError::FrameDecode(
76 "invalid Frame account discriminator".into(),
77 ));
78 }
79 let authority = Pubkey::try_from(&data[OFF_AUTHORITY..OFF_AUTHORITY + 32])
80 .map_err(|_| ScratchError::FrameDecode("invalid authority pubkey".into()))?;
81 let cursor = u32::from_le_bytes(data[OFF_CURSOR..OFF_CURSOR + 4].try_into().unwrap());
82 let index_count = u16::from_le_bytes(data[OFF_INDEX_COUNT..OFF_INDEX_COUNT + 2].try_into().unwrap());
83 let index_cap = u16::from_le_bytes(data[OFF_INDEX_CAP..OFF_INDEX_CAP + 2].try_into().unwrap());
84 let generation = u64::from_le_bytes(data[OFF_GENERATION..OFF_GENERATION + 8].try_into().unwrap());
85 let payload_at_len = u32::from_le_bytes(
86 data[OFF_PAYLOAD_AT_LEN..OFF_PAYLOAD_AT_LEN + 4]
87 .try_into()
88 .unwrap(),
89 );
90 let mut o = OFF_PAYLOAD_AT;
91 let mut payload_at = Vec::with_capacity(payload_at_len as usize);
92 for _ in 0..payload_at_len {
93 if o + 2 > data.len() {
94 return Err(ScratchError::FrameDecode("truncated payload_at".into()));
95 }
96 payload_at.push(u16::from_le_bytes(data[o..o + 2].try_into().unwrap()));
97 o += 2;
98 }
99 if payload_at_len != index_cap as u32 {
100 return Err(ScratchError::FrameDecode(format!(
101 "payload_at length mismatch: {payload_at_len} vs index_cap {index_cap}"
102 )));
103 }
104 if o + 4 > data.len() {
105 return Err(ScratchError::FrameDecode("truncated tape length".into()));
106 }
107 let tape_len = u32::from_le_bytes(data[o..o + 4].try_into().unwrap());
108 o += 4;
109 if o + tape_len as usize != data.len() {
110 return Err(ScratchError::FrameDecode("tape length mismatch".into()));
111 }
112 let tape = data[o..o + tape_len as usize].to_vec();
113 Ok(DecodedFrame {
114 authority,
115 cursor,
116 index_count,
117 index_cap,
118 generation,
119 payload_at,
120 tape,
121 })
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use ifx_core::wire::{Expr, LetBinding, ValueType};
128
129 #[test]
130 fn decode_rejects_short_data() {
131 assert!(decode_frame_account(&[ACCOUNT_DISC_FRAME]).is_err());
132 }
133
134 #[test]
135 fn read_u64_roundtrip_on_tape() {
136 let dec = DecodedFrame {
137 authority: Pubkey::new_unique(),
138 cursor: 8,
139 index_count: 1,
140 index_cap: 1,
141 generation: 0,
142 payload_at: vec![0],
143 tape: 1u64.to_le_bytes().to_vec(),
144 };
145 let sv = ScratchValue {
146 binding: LetBinding::Eval {
147 expr: Expr::ConstU64(1),
148 },
149 index: 0,
150 ty: ValueType::U64,
151 remaining: vec![],
152 };
153 assert_eq!(dec.read_u64(&sv).unwrap(), 1);
154 }
155}