1use crate::{LedgerCommitStore, constants::WASM_PAGE_SIZE_BYTES};
2use ic_platform::stable_structures::{Memory, Storable, storable::Bound};
3use std::borrow::Cow;
4use thiserror::Error;
5
6pub const STABLE_CELL_MAGIC: &[u8; 3] = b"SCL";
8pub const STABLE_CELL_LAYOUT_VERSION: u8 = 1;
10pub const STABLE_CELL_HEADER_SIZE: usize = 8;
12pub const STABLE_CELL_VALUE_OFFSET: u64 = 8;
14
15#[non_exhaustive]
22#[derive(Clone, Debug, Eq, Error, PartialEq)]
23pub enum StableCellPayloadError {
24 #[error("stable-cell ledger payload length {value_len} exceeds recovery limit")]
26 TooLarge { value_len: u64 },
27 #[error("memory is not an ic-stable-structures Cell")]
29 NotStableCell,
30 #[error("unexpected stable-cell layout version {version}")]
32 UnexpectedLayoutVersion {
33 version: u8,
35 },
36 #[error("stable-cell payload length {value_len} exceeds available bytes {available_bytes}")]
38 InvalidLength {
39 value_len: u64,
41 available_bytes: u64,
43 },
44}
45
46#[non_exhaustive]
56#[derive(Debug, Error)]
57pub enum StableCellLedgerError {
58 #[error(transparent)]
60 Payload(#[from] StableCellPayloadError),
61 #[error("stable-cell ledger record decode failed: {0}")]
63 Record(#[source] ic_wire::Error),
64}
65
66pub fn decode_stable_cell_payload<M: Memory>(
72 memory: &M,
73) -> Result<Vec<u8>, StableCellPayloadError> {
74 let value_len = stable_cell_payload_length(memory, crate::constants::MAX_LEDGER_RECORD_BYTES)?;
75 let mut bytes = vec![0; value_len];
76 memory.read(STABLE_CELL_VALUE_OFFSET, &mut bytes);
77 Ok(bytes)
78}
79
80pub fn stable_cell_payload_length<M: Memory>(
90 memory: &M,
91 maximum: usize,
92) -> Result<usize, StableCellPayloadError> {
93 if memory.size() == 0 {
94 return Err(StableCellPayloadError::NotStableCell);
95 }
96
97 let mut header = [0; STABLE_CELL_HEADER_SIZE];
98 memory.read(0, &mut header);
99 if &header[0..3] != STABLE_CELL_MAGIC {
100 return Err(StableCellPayloadError::NotStableCell);
101 }
102 if header[3] != STABLE_CELL_LAYOUT_VERSION {
103 return Err(StableCellPayloadError::UnexpectedLayoutVersion { version: header[3] });
104 }
105
106 let value_len = u64::from(u32::from_le_bytes([
107 header[4], header[5], header[6], header[7],
108 ]));
109 let available_bytes = memory.size().saturating_mul(WASM_PAGE_SIZE_BYTES);
110 let payload_capacity = available_bytes.saturating_sub(STABLE_CELL_VALUE_OFFSET);
111 if value_len > payload_capacity {
112 return Err(StableCellPayloadError::InvalidLength {
113 value_len,
114 available_bytes: payload_capacity,
115 });
116 }
117 if value_len > maximum as u64 {
118 return Err(StableCellPayloadError::TooLarge { value_len });
119 }
120 #[expect(
121 clippy::cast_possible_truncation,
122 reason = "the admitted caller ceiling is representable as usize"
123 )]
124 let value_len = value_len as usize;
125 Ok(value_len)
126}
127
128pub fn decode_stable_cell_commit_store(bytes: &[u8]) -> Result<LedgerCommitStore, ic_wire::Error> {
137 use crate::encoding::{RECORD_LIMITS, RECORD_MAGIC};
138 if bytes.len() > crate::constants::MAX_LEDGER_RECORD_BYTES {
139 return Err(ic_wire::Error::Limit);
140 }
141 let payload = bytes
142 .strip_prefix(RECORD_MAGIC)
143 .ok_or(ic_wire::Error::Malformed)?;
144 ic_wire::native::decode(payload, RECORD_LIMITS)
145}
146
147pub fn decode_stable_cell_commit_store_from_memory<M: Memory>(
155 memory: &M,
156) -> Result<LedgerCommitStore, StableCellLedgerError> {
157 if memory.size() == 0 {
158 return Ok(LedgerCommitStore::default());
159 }
160
161 let payload = decode_stable_cell_payload(memory)?;
162 decode_stable_cell_commit_store(&payload).map_err(StableCellLedgerError::Record)
163}
164
165pub struct EncodedLedgerRecord {
172 bytes: Vec<u8>,
173}
174
175impl EncodedLedgerRecord {
176 pub(crate) fn new(record: &LedgerCommitStore) -> Result<Self, ic_wire::Error> {
177 use crate::encoding::{RECORD_LIMITS, RECORD_MAGIC};
178 let payload = ic_wire::native::encode(record, RECORD_LIMITS)?;
179 let mut bytes = Vec::with_capacity(RECORD_MAGIC.len() + payload.len());
180 bytes.extend_from_slice(RECORD_MAGIC);
181 bytes.extend_from_slice(&payload);
182 Ok(Self { bytes })
183 }
184
185 pub(crate) const fn len(&self) -> usize {
186 self.bytes.len()
187 }
188}
189
190impl Storable for EncodedLedgerRecord {
191 const BOUND: Bound = Bound::Unbounded;
192
193 fn to_bytes(&self) -> Cow<'_, [u8]> {
194 Cow::Borrowed(&self.bytes)
195 }
196 fn into_bytes(self) -> Vec<u8> {
197 self.bytes
198 }
199 fn from_bytes(_: Cow<'_, [u8]>) -> Self {
200 panic!("publication bytes cannot recover ledger authority")
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::test_wire::hex_fixture;
208 use ic_platform::stable_structures::{Cell, VectorMemory};
209
210 #[test]
211 fn publication_refuses_oversized_payload_before_cell_mutation() {
212 let record = LedgerCommitStore {
213 current: Some(crate::physical::CommittedGenerationBytes::new(
214 1,
215 vec![0; crate::constants::MAX_COMMITTED_PAYLOAD_BYTES + 1],
216 )),
217 };
218 assert!(EncodedLedgerRecord::new(&record).is_err());
219 }
220
221 #[test]
222 fn stable_cell_ledger_record_round_trips_through_cell() {
223 let memory = VectorMemory::default();
224 let record = LedgerCommitStore::default();
225 drop(Cell::new(
226 memory.clone(),
227 EncodedLedgerRecord::new(&record).unwrap(),
228 ));
229 let payload = decode_stable_cell_payload(&memory).expect("decode stable cell payload");
230 let decoded = decode_stable_cell_commit_store(&payload).unwrap();
231 assert_eq!(decoded, record);
232 assert_eq!(
233 crate::decode_stable_cell_commit_store_from_memory(&memory).unwrap(),
234 record
235 );
236 }
237
238 #[test]
239 fn current_stable_cell_record_fixture_recovers() {
240 let bytes = hex_fixture(include_str!(
241 "../fixtures/current/ledger_commit_store_single.wire.hex"
242 ));
243 let record = decode_stable_cell_commit_store(&bytes).expect("stable-cell fixture");
244
245 assert_eq!(
246 EncodedLedgerRecord::new(&record).unwrap().len(),
247 bytes.len()
248 );
249 let encoded = EncodedLedgerRecord::new(&record).unwrap();
250 assert!(matches!(encoded.to_bytes(), Cow::Borrowed(_)));
251 let memory = VectorMemory::default();
252 drop(Cell::new(memory.clone(), encoded));
253 assert_eq!(decode_stable_cell_payload(&memory).unwrap(), bytes);
254 assert_eq!(
255 EncodedLedgerRecord::new(&record).unwrap().into_bytes(),
256 bytes
257 );
258 assert_eq!(
259 bytes,
260 EncodedLedgerRecord::new(&record).unwrap().into_bytes()
261 );
262 assert_eq!(
263 record
264 .recover()
265 .expect("fixture store recovers")
266 .current_generation(),
267 1
268 );
269 }
270
271 #[test]
272 fn malformed_payload_bytes_fail_closed_without_mutating_memory() {
273 use crate::encoding::RECORD_MAGIC;
274 let original = hex_fixture(include_str!(
275 "../fixtures/current/ledger_commit_store_single.wire.hex"
276 ));
277 let mut cases = vec![original[..original.len() - 1].to_vec()];
278 for tail in [vec![255; 32], vec![1], vec![2]] {
280 let mut bytes = RECORD_MAGIC.to_vec();
281 bytes.extend_from_slice(&tail);
282 cases.push(bytes);
283 }
284 for bytes in cases {
285 let memory = VectorMemory::default();
286 memory.grow(1);
287 memory.write(0, STABLE_CELL_MAGIC);
288 memory.write(3, &[STABLE_CELL_LAYOUT_VERSION]);
289 memory.write(4, &u32::try_from(bytes.len()).unwrap().to_le_bytes());
290 memory.write(STABLE_CELL_VALUE_OFFSET, &bytes);
291 let before = memory.borrow().clone();
292 for _ in 0..2 {
293 assert!(matches!(
294 decode_stable_cell_commit_store_from_memory(&memory),
295 Err(StableCellLedgerError::Record(_))
296 ));
297 }
298 assert_eq!(*memory.borrow(), before);
299 }
300 }
301
302 #[test]
303 fn stable_cell_ledger_record_rejects_trailing_bytes() {
304 let mut bytes = EncodedLedgerRecord::new(&LedgerCommitStore::default())
305 .unwrap()
306 .into_bytes();
307 bytes.push(0);
308
309 let err =
310 decode_stable_cell_commit_store(&bytes).expect_err("trailing bytes must fail closed");
311
312 assert_eq!(err, ic_wire::Error::Trailing);
313 }
314
315 #[test]
316 fn stable_cell_ledger_record_rejects_extra_top_level_data() {
317 let mut bytes = EncodedLedgerRecord::new(&LedgerCommitStore::default())
318 .unwrap()
319 .into_bytes();
320 bytes.push(1);
321 assert_eq!(
322 decode_stable_cell_commit_store(&bytes),
323 Err(ic_wire::Error::Trailing)
324 );
325 }
326
327 #[test]
328 fn stable_cell_ledger_record_requires_an_explicit_current_snapshot() {
329 assert!(decode_stable_cell_commit_store(crate::encoding::RECORD_MAGIC).is_err());
330 }
331
332 #[test]
333 fn stable_cell_payload_rejects_non_cell_memory() {
334 let memory = VectorMemory::default();
335 memory.grow(1);
336 memory.write(0, b"BAD");
337
338 assert_eq!(
339 decode_stable_cell_payload(&memory),
340 Err(StableCellPayloadError::NotStableCell)
341 );
342 }
343
344 #[test]
345 fn stable_cell_payload_rejects_empty_memory_without_panic() {
346 let memory = VectorMemory::default();
347
348 assert_eq!(
349 decode_stable_cell_payload(&memory),
350 Err(StableCellPayloadError::NotStableCell)
351 );
352 assert_eq!(
353 crate::decode_stable_cell_commit_store_from_memory(&memory).unwrap(),
354 LedgerCommitStore::default()
355 );
356 assert_eq!(
357 memory.size(),
358 0,
359 "reading empty memory must not initialize it"
360 );
361 }
362
363 #[test]
364 fn stable_cell_ledger_memory_reader_classifies_bad_record_without_panic() {
365 let memory = VectorMemory::default();
366 memory.grow(1);
367 memory.write(0, STABLE_CELL_MAGIC);
368 memory.write(3, &[STABLE_CELL_LAYOUT_VERSION]);
369 memory.write(4, &1_u32.to_le_bytes());
370 memory.write(STABLE_CELL_VALUE_OFFSET, &[0xff]);
371
372 let err = decode_stable_cell_commit_store_from_memory(&memory)
373 .expect_err("bad record must be classified");
374
375 assert!(matches!(err, StableCellLedgerError::Record(_)));
376 }
377 #[test]
378 fn oversized_cell_length_is_rejected_before_payload_read() {
379 struct HeaderOnly {
380 value_len: u32,
381 pages: u64,
382 }
383 impl Memory for HeaderOnly {
384 fn size(&self) -> u64 {
385 self.pages
386 }
387 fn grow(&self, _: u64) -> i64 {
388 panic!("must not grow")
389 }
390 fn write(&self, _: u64, _: &[u8]) {
391 panic!("must not write")
392 }
393 fn read(&self, offset: u64, bytes: &mut [u8]) {
394 assert_eq!(offset, 0);
395 assert_eq!(bytes.len(), STABLE_CELL_HEADER_SIZE);
396 bytes[..4].copy_from_slice(b"SCL\x01");
397 bytes[4..8].copy_from_slice(&self.value_len.to_le_bytes());
398 }
399 }
400 for value_len in [
401 u32::try_from(crate::constants::MAX_LEDGER_RECORD_BYTES + 1).unwrap(),
402 u32::MAX,
403 ] {
404 let memory = HeaderOnly {
406 value_len,
407 pages: 65_537,
408 };
409 assert!(matches!(
410 decode_stable_cell_commit_store_from_memory(&memory),
411 Err(StableCellLedgerError::Payload(StableCellPayloadError::TooLarge {
412 value_len: rejected,
413 })) if rejected == u64::from(value_len)
414 ));
415 let memory = HeaderOnly {
417 value_len,
418 pages: 1,
419 };
420 assert_eq!(
421 decode_stable_cell_payload(&memory),
422 Err(StableCellPayloadError::InvalidLength {
423 value_len: u64::from(value_len),
424 available_bytes: WASM_PAGE_SIZE_BYTES - STABLE_CELL_VALUE_OFFSET,
425 })
426 );
427 }
428 for value_len in [16_385, u32::MAX] {
430 let memory = HeaderOnly {
431 value_len,
432 pages: 65_537,
433 };
434 assert_eq!(
435 stable_cell_payload_length(&memory, 16_384),
436 Err(StableCellPayloadError::TooLarge {
437 value_len: u64::from(value_len)
438 }),
439 );
440 }
441 }
442
443 #[test]
444 fn hostile_wire_lengths_reject_on_production_record_decode() {
445 use crate::encoding::{RECORD_LIMITS, RECORD_MAGIC};
446 let prefix = ic_wire::native::encode(&Some((1_u64, 0_u64, 0_u64)), RECORD_LIMITS).unwrap();
448 for length in [
449 crate::constants::MAX_COMMITTED_PAYLOAD_BYTES + 1,
450 usize::MAX,
451 ] {
452 let mut bytes = RECORD_MAGIC.to_vec();
453 bytes.extend_from_slice(&prefix);
454 bytes.extend(ic_wire::native::encode(&length, RECORD_LIMITS).unwrap());
455 assert!(decode_stable_cell_commit_store(&bytes).is_err());
456 }
457 let payload = vec![0; crate::constants::MAX_COMMITTED_PAYLOAD_BYTES + 1];
459 let oversized =
460 ic_wire::BytesRef::<{ crate::constants::MAX_COMMITTED_PAYLOAD_BYTES + 1 }>::new(
461 &payload,
462 )
463 .unwrap();
464 let mut bytes = RECORD_MAGIC.to_vec();
465 bytes.extend(
466 ic_wire::native::encode(&Some((1_u64, 0_u64, 0_u64, oversized)), RECORD_LIMITS)
467 .unwrap(),
468 );
469 assert!(decode_stable_cell_commit_store(&bytes).is_err());
470 assert_eq!(
471 decode_stable_cell_commit_store(&vec![
472 0;
473 crate::constants::MAX_LEDGER_RECORD_BYTES + 1
474 ]),
475 Err(ic_wire::Error::Limit)
476 );
477 }
478}