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