1use crate::{LedgerCommitStore, constants::WASM_PAGE_SIZE_BYTES};
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;
15
16#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
27#[serde(deny_unknown_fields)]
28pub struct StableCellLedgerRecord {
29 store: LedgerCommitStore,
30}
31
32impl StableCellLedgerRecord {
33 #[must_use]
35 pub const fn new(store: LedgerCommitStore) -> Self {
36 Self { store }
37 }
38
39 #[must_use]
41 pub const fn store(&self) -> &LedgerCommitStore {
42 &self.store
43 }
44
45 pub const fn store_mut(&mut self) -> &mut LedgerCommitStore {
47 &mut self.store
48 }
49
50 #[must_use]
52 pub fn into_store(self) -> LedgerCommitStore {
53 self.store
54 }
55}
56
57impl Storable for StableCellLedgerRecord {
58 const BOUND: Bound = Bound::Unbounded;
59
60 fn to_bytes(&self) -> Cow<'_, [u8]> {
61 Cow::Owned(serialize_record(self))
62 }
63
64 fn into_bytes(self) -> Vec<u8> {
65 serialize_record(&self)
66 }
67
68 fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
69 decode_stable_cell_ledger_record(&bytes).unwrap_or_else(|err| {
70 panic!("StableCellLedgerRecord deserialize failed: {err}");
71 })
72 }
73}
74
75#[non_exhaustive]
80#[derive(Clone, Debug, Eq, Error, PartialEq)]
81pub enum StableCellPayloadError {
82 #[error("memory is not an ic-stable-structures Cell")]
84 NotStableCell,
85 #[error("unexpected stable-cell layout version {version}")]
87 UnexpectedLayoutVersion {
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#[non_exhaustive]
112#[derive(Debug, Error)]
113pub enum StableCellLedgerError {
114 #[error(transparent)]
116 Payload(#[from] StableCellPayloadError),
117 #[error("stable-cell ledger record decode failed")]
119 Record(#[source] ciborium::de::Error<std::io::Error>),
120}
121
122pub fn decode_stable_cell_payload<M: Memory>(
128 memory: &M,
129) -> Result<Vec<u8>, StableCellPayloadError> {
130 if memory.size() == 0 {
131 return Err(StableCellPayloadError::NotStableCell);
132 }
133
134 let mut header = [0; STABLE_CELL_HEADER_SIZE];
135 memory.read(0, &mut header);
136 if &header[0..3] != STABLE_CELL_MAGIC {
137 return Err(StableCellPayloadError::NotStableCell);
138 }
139 if header[3] != STABLE_CELL_LAYOUT_VERSION {
140 return Err(StableCellPayloadError::UnexpectedLayoutVersion { version: header[3] });
141 }
142
143 let value_len = u64::from(u32::from_le_bytes([
144 header[4], header[5], header[6], header[7],
145 ]));
146 let available_bytes = memory.size().saturating_mul(WASM_PAGE_SIZE_BYTES);
147 let payload_capacity = available_bytes.saturating_sub(STABLE_CELL_VALUE_OFFSET);
148 if value_len > payload_capacity {
149 return Err(StableCellPayloadError::InvalidLength {
150 value_len,
151 available_bytes: payload_capacity,
152 });
153 }
154 let value_len = usize::try_from(value_len)
155 .map_err(|_| StableCellPayloadError::LengthOverflow { value_len })?;
156
157 let mut bytes = vec![0; value_len];
158 memory.read(STABLE_CELL_VALUE_OFFSET, &mut bytes);
159 Ok(bytes)
160}
161
162pub fn decode_stable_cell_ledger_record(
171 bytes: &[u8],
172) -> Result<StableCellLedgerRecord, ciborium::de::Error<std::io::Error>> {
173 crate::cbor::from_slice_exact(bytes)
174}
175
176pub fn decode_stable_cell_ledger_record_from_memory<M: Memory>(
177 memory: &M,
178) -> Result<StableCellLedgerRecord, StableCellLedgerError> {
179 if memory.size() == 0 {
180 return Ok(StableCellLedgerRecord::default());
181 }
182
183 let payload = decode_stable_cell_payload(memory)?;
184 decode_stable_cell_ledger_record(&payload).map_err(StableCellLedgerError::Record)
185}
186
187pub fn validate_stable_cell_ledger_memory<M: Memory>(
195 memory: &M,
196) -> Result<(), StableCellLedgerError> {
197 decode_stable_cell_ledger_record_from_memory(memory)?;
198 Ok(())
199}
200
201fn serialize_record(record: &StableCellLedgerRecord) -> Vec<u8> {
202 let mut bytes = Vec::new();
203 ciborium::into_writer(record, &mut bytes).unwrap_or_else(|err| {
204 panic!("StableCellLedgerRecord serialize failed: {err}");
205 });
206 bytes
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use ic_stable_structures::{Cell, VectorMemory};
213
214 fn hex_fixture(contents: &str) -> Vec<u8> {
215 let hex = contents
216 .chars()
217 .filter(|char| !char.is_whitespace())
218 .collect::<String>();
219 assert_eq!(hex.len() % 2, 0, "fixture hex must have byte pairs");
220 hex.as_bytes()
221 .chunks_exact(2)
222 .map(|pair| {
223 let pair = std::str::from_utf8(pair).expect("fixture hex is utf8");
224 u8::from_str_radix(pair, 16).expect("fixture hex byte")
225 })
226 .collect()
227 }
228
229 #[test]
230 fn stable_cell_ledger_record_round_trips_through_cell() {
231 let memory = VectorMemory::default();
232 let record = StableCellLedgerRecord::default();
233 let cell = Cell::init(memory.clone(), record.clone());
234
235 assert_eq!(cell.get(), &record);
236 let payload = decode_stable_cell_payload(&memory).expect("decode stable cell payload");
237 let decoded = StableCellLedgerRecord::from_bytes(Cow::Owned(payload));
238 assert_eq!(decoded, record);
239 }
240
241 #[test]
242 fn current_stable_cell_record_fixture_recovers() {
243 let bytes = hex_fixture(include_str!(
244 "../fixtures/current/stable_cell_record.cbor.hex"
245 ));
246 let record = decode_stable_cell_ledger_record(&bytes).expect("stable-cell fixture");
247
248 assert_eq!(
249 bytes,
250 crate::test_cbor::to_vec(&record).expect("re-encoded stable-cell fixture")
251 );
252 assert_eq!(
253 record
254 .store()
255 .recover()
256 .expect("fixture store recovers")
257 .current_generation(),
258 1
259 );
260 }
261
262 #[test]
263 fn stable_cell_ledger_record_rejects_trailing_bytes() {
264 let mut bytes = serialize_record(&StableCellLedgerRecord::default());
265 bytes.push(0);
266
267 let err =
268 decode_stable_cell_ledger_record(&bytes).expect_err("trailing bytes must fail closed");
269
270 assert!(err.to_string().contains("trailing bytes"));
271 }
272
273 #[test]
274 fn stable_cell_ledger_record_rejects_unknown_top_level_fields() {
275 use crate::test_cbor::Value;
276
277 let mut map = Vec::new();
278 crate::test_cbor::map_insert(
279 &mut map,
280 Value::Text("store".to_string()),
281 crate::test_cbor::to_value(LedgerCommitStore::default()).expect("store value"),
282 );
283 crate::test_cbor::map_insert(
284 &mut map,
285 Value::Text("future_field".to_string()),
286 Value::Bool(true),
287 );
288 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("unknown-field stable cell");
289
290 let err = decode_stable_cell_ledger_record(&bytes)
291 .expect_err("unknown stable-cell record field must fail closed");
292
293 assert!(err.to_string().contains("future_field"));
294 }
295
296 #[test]
297 fn stable_cell_ledger_record_requires_both_commit_slot_fields() {
298 use crate::test_cbor::Value;
299
300 for (missing, present) in [("slot0", "slot1"), ("slot1", "slot0")] {
301 let mut physical = Vec::new();
302 crate::test_cbor::map_insert(
303 &mut physical,
304 Value::Text(present.to_string()),
305 Value::Null,
306 );
307 let mut store = Vec::new();
308 crate::test_cbor::map_insert(
309 &mut store,
310 Value::Text("physical".to_string()),
311 Value::Map(physical),
312 );
313 let mut record = Vec::new();
314 crate::test_cbor::map_insert(
315 &mut record,
316 Value::Text("store".to_string()),
317 Value::Map(store),
318 );
319 let bytes = crate::test_cbor::to_vec(&Value::Map(record)).expect("record bytes");
320
321 let err = decode_stable_cell_ledger_record(&bytes)
322 .expect_err("missing commit slot must fail closed");
323
324 assert!(err.to_string().contains(missing));
325 }
326 }
327
328 #[test]
329 fn stable_cell_payload_rejects_non_cell_memory() {
330 let memory = VectorMemory::default();
331 memory.grow(1);
332 memory.write(0, b"BAD");
333
334 assert_eq!(
335 decode_stable_cell_payload(&memory),
336 Err(StableCellPayloadError::NotStableCell)
337 );
338 }
339
340 #[test]
341 fn stable_cell_payload_rejects_empty_memory_without_panic() {
342 let memory = VectorMemory::default();
343
344 assert_eq!(
345 decode_stable_cell_payload(&memory),
346 Err(StableCellPayloadError::NotStableCell)
347 );
348 }
349
350 #[test]
351 fn stable_cell_ledger_preflight_classifies_bad_record_without_panic() {
352 let memory = VectorMemory::default();
353 memory.grow(1);
354 memory.write(0, STABLE_CELL_MAGIC);
355 memory.write(3, &[STABLE_CELL_LAYOUT_VERSION]);
356 memory.write(4, &1_u32.to_le_bytes());
357 memory.write(STABLE_CELL_VALUE_OFFSET, &[0xff]);
358
359 let err =
360 validate_stable_cell_ledger_memory(&memory).expect_err("bad record must be classified");
361
362 assert!(matches!(err, StableCellLedgerError::Record(_)));
363 }
364}