1use crate::LedgerCommitStore;
2use ic_stable_structures::{Memory, Storable, storable::Bound};
3use serde::{Deserialize, Serialize};
4use std::borrow::Cow;
5use thiserror::Error;
6
7pub const STABLE_CELL_MAGIC: &[u8; 3] = b"SCL";
9pub const STABLE_CELL_LAYOUT_VERSION: u8 = 1;
11pub const STABLE_CELL_HEADER_SIZE: usize = 8;
13pub const STABLE_CELL_VALUE_OFFSET: u64 = 8;
15const WASM_PAGE_SIZE: u64 = 65_536;
16
17#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
28#[serde(deny_unknown_fields)]
29pub struct StableCellLedgerRecord {
30 store: LedgerCommitStore,
31}
32
33impl StableCellLedgerRecord {
34 #[must_use]
36 pub const fn new(store: LedgerCommitStore) -> Self {
37 Self { store }
38 }
39
40 #[must_use]
42 pub const fn store(&self) -> &LedgerCommitStore {
43 &self.store
44 }
45
46 pub const fn store_mut(&mut self) -> &mut LedgerCommitStore {
48 &mut self.store
49 }
50
51 #[must_use]
53 pub fn into_store(self) -> LedgerCommitStore {
54 self.store
55 }
56}
57
58impl Storable for StableCellLedgerRecord {
59 const BOUND: Bound = Bound::Unbounded;
60
61 fn to_bytes(&self) -> Cow<'_, [u8]> {
62 Cow::Owned(serialize_record(self))
63 }
64
65 fn into_bytes(self) -> Vec<u8> {
66 serialize_record(&self)
67 }
68
69 fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
70 decode_stable_cell_ledger_record(&bytes).unwrap_or_else(|err| {
71 panic!("StableCellLedgerRecord deserialize failed: {err}");
72 })
73 }
74}
75
76#[derive(Clone, Debug, Eq, Error, PartialEq)]
81pub enum StableCellPayloadError {
82 #[error("memory is not an ic-stable-structures Cell")]
84 NotStableCell,
85 #[error("unsupported stable-cell layout version {version}")]
87 UnsupportedVersion {
88 version: u8,
90 },
91 #[error("stable-cell payload length {value_len} exceeds available bytes {available_bytes}")]
93 InvalidLength {
94 value_len: u64,
96 available_bytes: u64,
98 },
99 #[error("stable-cell payload length {value_len} cannot fit in usize")]
101 LengthOverflow {
102 value_len: u64,
104 },
105}
106
107#[derive(Debug, Error)]
112pub enum StableCellLedgerError {
113 #[error(transparent)]
115 Payload(#[from] StableCellPayloadError),
116 #[error("stable-cell ledger record decode failed")]
118 Record(#[source] serde_cbor::Error),
119}
120
121pub fn decode_stable_cell_payload<M: Memory>(
127 memory: &M,
128) -> Result<Vec<u8>, StableCellPayloadError> {
129 let mut header = [0; STABLE_CELL_HEADER_SIZE];
130 memory.read(0, &mut header);
131 if &header[0..3] != STABLE_CELL_MAGIC {
132 return Err(StableCellPayloadError::NotStableCell);
133 }
134 if header[3] != STABLE_CELL_LAYOUT_VERSION {
135 return Err(StableCellPayloadError::UnsupportedVersion { version: header[3] });
136 }
137
138 let value_len = u64::from(u32::from_le_bytes([
139 header[4], header[5], header[6], header[7],
140 ]));
141 let available_bytes = memory.size().saturating_mul(WASM_PAGE_SIZE);
142 let payload_capacity = available_bytes.saturating_sub(STABLE_CELL_VALUE_OFFSET);
143 if value_len > payload_capacity {
144 return Err(StableCellPayloadError::InvalidLength {
145 value_len,
146 available_bytes: payload_capacity,
147 });
148 }
149 let value_len = usize::try_from(value_len)
150 .map_err(|_| StableCellPayloadError::LengthOverflow { value_len })?;
151
152 let mut bytes = vec![0; value_len];
153 memory.read(STABLE_CELL_VALUE_OFFSET, &mut bytes);
154 Ok(bytes)
155}
156
157pub fn decode_stable_cell_ledger_record(
166 bytes: &[u8],
167) -> Result<StableCellLedgerRecord, serde_cbor::Error> {
168 serde_cbor::from_slice(bytes)
169}
170
171pub fn validate_stable_cell_ledger_memory<M: Memory>(
179 memory: &M,
180) -> Result<(), StableCellLedgerError> {
181 if memory.size() == 0 {
182 return Ok(());
183 }
184
185 let payload = decode_stable_cell_payload(memory)?;
186 decode_stable_cell_ledger_record(&payload).map_err(StableCellLedgerError::Record)?;
187 Ok(())
188}
189
190fn serialize_record(record: &StableCellLedgerRecord) -> Vec<u8> {
191 serde_cbor::to_vec(record).unwrap_or_else(|err| {
192 panic!("StableCellLedgerRecord serialize failed: {err}");
193 })
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use ic_stable_structures::{Cell, VectorMemory};
200
201 fn hex_fixture(contents: &str) -> Vec<u8> {
202 let hex = contents
203 .chars()
204 .filter(|char| !char.is_whitespace())
205 .collect::<String>();
206 assert_eq!(hex.len() % 2, 0, "fixture hex must have byte pairs");
207 hex.as_bytes()
208 .chunks_exact(2)
209 .map(|pair| {
210 let pair = std::str::from_utf8(pair).expect("fixture hex is utf8");
211 u8::from_str_radix(pair, 16).expect("fixture hex byte")
212 })
213 .collect()
214 }
215
216 #[test]
217 fn stable_cell_ledger_record_round_trips_through_cell() {
218 let memory = VectorMemory::default();
219 let record = StableCellLedgerRecord::default();
220 let cell = Cell::init(memory.clone(), record.clone());
221
222 assert_eq!(cell.get(), &record);
223 let payload = decode_stable_cell_payload(&memory).expect("decode stable cell payload");
224 let decoded = StableCellLedgerRecord::from_bytes(Cow::Owned(payload));
225 assert_eq!(decoded, record);
226 }
227
228 #[test]
229 fn v1_stable_cell_record_fixture_recovers() {
230 let bytes = hex_fixture(include_str!("../fixtures/v1/stable_cell_record.cbor.hex"));
231 let record = decode_stable_cell_ledger_record(&bytes).expect("stable-cell fixture");
232
233 assert_eq!(
234 bytes,
235 serde_cbor::to_vec(&record).expect("re-encoded stable-cell fixture")
236 );
237 assert_eq!(
238 record
239 .store()
240 .recover()
241 .expect("fixture store recovers")
242 .current_generation(),
243 1
244 );
245 }
246
247 #[test]
248 fn stable_cell_payload_rejects_non_cell_memory() {
249 let memory = VectorMemory::default();
250 memory.grow(1);
251 memory.write(0, b"BAD");
252
253 assert_eq!(
254 decode_stable_cell_payload(&memory),
255 Err(StableCellPayloadError::NotStableCell)
256 );
257 }
258
259 #[test]
260 fn stable_cell_ledger_preflight_classifies_bad_record_without_panic() {
261 let memory = VectorMemory::default();
262 memory.grow(1);
263 memory.write(0, STABLE_CELL_MAGIC);
264 memory.write(3, &[STABLE_CELL_LAYOUT_VERSION]);
265 memory.write(4, &1_u32.to_le_bytes());
266 memory.write(STABLE_CELL_VALUE_OFFSET, &[0xff]);
267
268 let err =
269 validate_stable_cell_ledger_memory(&memory).expect_err("bad record must be classified");
270
271 assert!(matches!(err, StableCellLedgerError::Record(_)));
272 }
273}