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("stable-cell ledger payload length {value_len} exceeds recovery limit")]
84 TooLarge { value_len: u64 },
85 #[error("memory is not an ic-stable-structures Cell")]
87 NotStableCell,
88 #[error("unexpected stable-cell layout version {version}")]
90 UnexpectedLayoutVersion {
91 version: u8,
93 },
94 #[error("stable-cell payload length {value_len} exceeds available bytes {available_bytes}")]
96 InvalidLength {
97 value_len: u64,
99 available_bytes: u64,
101 },
102 #[error("stable-cell payload length {value_len} cannot fit in usize")]
104 LengthOverflow {
105 value_len: u64,
107 },
108}
109
110#[non_exhaustive]
115#[derive(Debug, Error)]
116pub enum StableCellLedgerError {
117 #[error(transparent)]
119 Payload(#[from] StableCellPayloadError),
120 #[error("stable-cell ledger record decode failed")]
122 Record(#[source] ciborium::de::Error<std::io::Error>),
123}
124
125pub fn decode_stable_cell_payload<M: Memory>(
131 memory: &M,
132) -> Result<Vec<u8>, StableCellPayloadError> {
133 if memory.size() == 0 {
134 return Err(StableCellPayloadError::NotStableCell);
135 }
136
137 let mut header = [0; STABLE_CELL_HEADER_SIZE];
138 memory.read(0, &mut header);
139 if &header[0..3] != STABLE_CELL_MAGIC {
140 return Err(StableCellPayloadError::NotStableCell);
141 }
142 if header[3] != STABLE_CELL_LAYOUT_VERSION {
143 return Err(StableCellPayloadError::UnexpectedLayoutVersion { version: header[3] });
144 }
145
146 let value_len = u64::from(u32::from_le_bytes([
147 header[4], header[5], header[6], header[7],
148 ]));
149 let available_bytes = memory.size().saturating_mul(WASM_PAGE_SIZE_BYTES);
150 let payload_capacity = available_bytes.saturating_sub(STABLE_CELL_VALUE_OFFSET);
151 if value_len > payload_capacity {
152 return Err(StableCellPayloadError::InvalidLength {
153 value_len,
154 available_bytes: payload_capacity,
155 });
156 }
157 if value_len > crate::constants::MAX_LEDGER_RECORD_BYTES as u64 {
158 return Err(StableCellPayloadError::TooLarge { value_len });
159 }
160 let value_len = usize::try_from(value_len)
161 .map_err(|_| StableCellPayloadError::LengthOverflow { value_len })?;
162
163 let mut bytes = vec![0; value_len];
164 memory.read(STABLE_CELL_VALUE_OFFSET, &mut bytes);
165 Ok(bytes)
166}
167
168pub fn decode_stable_cell_ledger_record(
177 bytes: &[u8],
178) -> Result<StableCellLedgerRecord, ciborium::de::Error<std::io::Error>> {
179 crate::cbor::from_slice_exact(bytes)
180}
181
182pub fn decode_stable_cell_ledger_record_from_memory<M: Memory>(
183 memory: &M,
184) -> Result<StableCellLedgerRecord, StableCellLedgerError> {
185 if memory.size() == 0 {
186 return Ok(StableCellLedgerRecord::default());
187 }
188
189 let payload = decode_stable_cell_payload(memory)?;
190 decode_stable_cell_ledger_record(&payload).map_err(StableCellLedgerError::Record)
191}
192
193pub fn validate_stable_cell_ledger_memory<M: Memory>(
201 memory: &M,
202) -> Result<(), StableCellLedgerError> {
203 decode_stable_cell_ledger_record_from_memory(memory)?;
204 Ok(())
205}
206
207fn serialize_record(record: &StableCellLedgerRecord) -> Vec<u8> {
208 let mut bytes = Vec::new();
209 ciborium::into_writer(record, &mut bytes).unwrap_or_else(|err| {
210 panic!("StableCellLedgerRecord serialize failed: {err}");
211 });
212 bytes
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use ic_stable_structures::{Cell, VectorMemory};
219
220 fn hex_fixture(contents: &str) -> Vec<u8> {
221 let hex = contents
222 .chars()
223 .filter(|char| !char.is_whitespace())
224 .collect::<String>();
225 assert_eq!(hex.len() % 2, 0, "fixture hex must have byte pairs");
226 hex.as_bytes()
227 .as_chunks::<2>()
228 .0
229 .iter()
230 .map(|pair| {
231 let pair = std::str::from_utf8(pair).expect("fixture hex is utf8");
232 u8::from_str_radix(pair, 16).expect("fixture hex byte")
233 })
234 .collect()
235 }
236
237 #[test]
238 fn stable_cell_ledger_record_round_trips_through_cell() {
239 let memory = VectorMemory::default();
240 let record = StableCellLedgerRecord::default();
241 let cell = Cell::init(memory.clone(), record.clone());
242
243 assert_eq!(cell.get(), &record);
244 let payload = decode_stable_cell_payload(&memory).expect("decode stable cell payload");
245 let decoded = StableCellLedgerRecord::from_bytes(Cow::Owned(payload));
246 assert_eq!(decoded, record);
247 }
248
249 #[test]
250 fn current_stable_cell_record_fixture_recovers() {
251 let bytes = hex_fixture(include_str!(
252 "../fixtures/current/stable_cell_record.cbor.hex"
253 ));
254 let record = decode_stable_cell_ledger_record(&bytes).expect("stable-cell fixture");
255
256 assert_eq!(
257 bytes,
258 crate::test_cbor::to_vec(&record).expect("re-encoded stable-cell fixture")
259 );
260 assert_eq!(
261 record
262 .store()
263 .recover()
264 .expect("fixture store recovers")
265 .current_generation(),
266 1
267 );
268 }
269
270 #[test]
271 fn malformed_payload_bytes_fail_closed_without_mutating_memory() {
272 let original = hex_fixture(include_str!(
273 "../fixtures/current/stable_cell_record.cbor.hex"
274 ));
275 let start = original
276 .windows(8)
277 .position(|bytes| bytes == b"\x67payload")
278 .unwrap()
279 + 8;
280 let oversized = u32::try_from(crate::constants::MAX_COMMITTED_PAYLOAD_BYTES + 1).unwrap();
281 let mut cases = Vec::new();
282 for replacement in [
283 vec![0x5a, 0xff, 0xff, 0xff, 0xff],
284 vec![0x5f, 0xff],
285 vec![0x41],
286 vec![0xff],
287 ] {
288 let mut bytes = original[..start].to_vec();
289 bytes.extend(replacement);
290 cases.push(bytes);
291 }
292 let mut bytes = original[..start].to_vec();
293 bytes.push(0x5a);
294 bytes.extend_from_slice(&oversized.to_be_bytes());
295 bytes.resize(bytes.len() + oversized as usize, 0);
296 cases.push(bytes);
297 for bytes in cases {
298 let memory = VectorMemory::default();
299 let pages = (bytes.len() + STABLE_CELL_HEADER_SIZE).div_ceil(65_536);
300 memory.grow(pages as u64);
301 memory.write(0, STABLE_CELL_MAGIC);
302 memory.write(3, &[STABLE_CELL_LAYOUT_VERSION]);
303 memory.write(4, &u32::try_from(bytes.len()).unwrap().to_le_bytes());
304 memory.write(STABLE_CELL_VALUE_OFFSET, &bytes);
305 let before = memory.borrow().clone();
306 for _ in 0..2 {
307 assert!(matches!(
308 decode_stable_cell_ledger_record_from_memory(&memory),
309 Err(StableCellLedgerError::Record(_))
310 ));
311 assert!(matches!(
312 validate_stable_cell_ledger_memory(&memory),
313 Err(StableCellLedgerError::Record(_))
314 ));
315 }
316 assert_eq!(*memory.borrow(), before);
317 }
318 }
319
320 #[test]
321 fn stable_cell_ledger_record_rejects_trailing_bytes() {
322 let mut bytes = serialize_record(&StableCellLedgerRecord::default());
323 bytes.push(0);
324
325 let err =
326 decode_stable_cell_ledger_record(&bytes).expect_err("trailing bytes must fail closed");
327
328 assert!(err.to_string().contains("trailing bytes"));
329 }
330
331 #[test]
332 fn stable_cell_ledger_record_rejects_unknown_top_level_fields() {
333 use crate::test_cbor::Value;
334
335 let mut map = Vec::new();
336 crate::test_cbor::map_insert(
337 &mut map,
338 Value::Text("store".to_string()),
339 crate::test_cbor::to_value(LedgerCommitStore::default()).expect("store value"),
340 );
341 crate::test_cbor::map_insert(
342 &mut map,
343 Value::Text("future_field".to_string()),
344 Value::Bool(true),
345 );
346 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("unknown-field stable cell");
347
348 let err = decode_stable_cell_ledger_record(&bytes)
349 .expect_err("unknown stable-cell record field must fail closed");
350
351 assert!(err.to_string().contains("future_field"));
352 }
353
354 #[test]
355 fn stable_cell_ledger_record_requires_both_commit_slot_fields() {
356 use crate::test_cbor::Value;
357
358 for (missing, present) in [("slot0", "slot1"), ("slot1", "slot0")] {
359 let mut physical = Vec::new();
360 crate::test_cbor::map_insert(
361 &mut physical,
362 Value::Text(present.to_string()),
363 Value::Null,
364 );
365 let mut store = Vec::new();
366 crate::test_cbor::map_insert(
367 &mut store,
368 Value::Text("physical".to_string()),
369 Value::Map(physical),
370 );
371 let mut record = Vec::new();
372 crate::test_cbor::map_insert(
373 &mut record,
374 Value::Text("store".to_string()),
375 Value::Map(store),
376 );
377 let bytes = crate::test_cbor::to_vec(&Value::Map(record)).expect("record bytes");
378
379 let err = decode_stable_cell_ledger_record(&bytes)
380 .expect_err("missing commit slot must fail closed");
381
382 assert!(err.to_string().contains(missing));
383 }
384 }
385
386 #[test]
387 fn stable_cell_payload_rejects_non_cell_memory() {
388 let memory = VectorMemory::default();
389 memory.grow(1);
390 memory.write(0, b"BAD");
391
392 assert_eq!(
393 decode_stable_cell_payload(&memory),
394 Err(StableCellPayloadError::NotStableCell)
395 );
396 }
397
398 #[test]
399 fn stable_cell_payload_rejects_empty_memory_without_panic() {
400 let memory = VectorMemory::default();
401
402 assert_eq!(
403 decode_stable_cell_payload(&memory),
404 Err(StableCellPayloadError::NotStableCell)
405 );
406 }
407
408 #[test]
409 fn stable_cell_ledger_preflight_classifies_bad_record_without_panic() {
410 let memory = VectorMemory::default();
411 memory.grow(1);
412 memory.write(0, STABLE_CELL_MAGIC);
413 memory.write(3, &[STABLE_CELL_LAYOUT_VERSION]);
414 memory.write(4, &1_u32.to_le_bytes());
415 memory.write(STABLE_CELL_VALUE_OFFSET, &[0xff]);
416
417 let err =
418 validate_stable_cell_ledger_memory(&memory).expect_err("bad record must be classified");
419
420 assert!(matches!(err, StableCellLedgerError::Record(_)));
421 }
422 #[test]
423 fn oversized_cell_length_is_rejected_before_payload_read() {
424 struct HeaderOnly;
425 impl Memory for HeaderOnly {
426 fn size(&self) -> u64 {
427 2048
428 }
429 fn grow(&self, _: u64) -> i64 {
430 panic!("must not grow")
431 }
432 fn write(&self, _: u64, _: &[u8]) {
433 panic!("must not write")
434 }
435 fn read(&self, offset: u64, bytes: &mut [u8]) {
436 assert_eq!(offset, 0);
437 assert_eq!(bytes.len(), STABLE_CELL_HEADER_SIZE);
438 bytes[..4].copy_from_slice(b"SCL\x01");
439 bytes[4..8].copy_from_slice(
440 &u32::try_from(crate::constants::MAX_LEDGER_RECORD_BYTES + 1)
441 .unwrap()
442 .to_le_bytes(),
443 );
444 }
445 }
446 assert!(matches!(
447 decode_stable_cell_ledger_record_from_memory(&HeaderOnly),
448 Err(StableCellLedgerError::Payload(
449 StableCellPayloadError::TooLarge { .. }
450 ))
451 ));
452 }
453
454 #[test]
455 fn hostile_cbor_is_rejected_on_production_record_decode() {
456 let huge_array = [0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
457 let huge_string = [0x7b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
458 let mut nested = vec![0x81; crate::constants::MAX_LEDGER_NESTING + 1];
459 nested.push(0);
460 for bytes in [&huge_array[..], &huge_string[..], &nested, &[0xa1], &[0xff]] {
461 assert!(decode_stable_cell_ledger_record(bytes).is_err());
462 }
463 }
464}