Skip to main content

ic_storage/
stable_cell.rs

1use crate::{Memory, WASM_PAGE_SIZE_BYTES};
2use thiserror::Error;
3
4/// Stable-cell magic prefix written by `Storage Cell`.
5pub const STABLE_CELL_MAGIC: &[u8; 8] = b"ICCELL01";
6/// Stable-cell header byte length.
7pub const STABLE_CELL_HEADER_SIZE: usize = 24;
8/// Byte offset where the stable-cell value payload starts.
9pub const STABLE_CELL_VALUE_OFFSET: u64 = STABLE_CELL_HEADER_SIZE as u64;
10
11///
12/// StableCellPayloadError
13///
14/// Stable-cell payload decode failure.
15///
16
17#[non_exhaustive]
18#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
19pub enum StableCellPayloadError {
20    /// Declared bytes exceed the current recovery ceiling before allocation.
21    #[error("stable-cell payload length {value_len} exceeds the caller's limit")]
22    TooLarge { value_len: u64 },
23    /// Memory contents do not start with the stable-cell marker.
24    #[error("memory is not a Storage Cell")]
25    NotStableCell,
26    /// Stable-cell header length does not fit inside the memory.
27    #[error("stable-cell payload length {value_len} exceeds available bytes {available_bytes}")]
28    InvalidLength {
29        /// Encoded value length.
30        value_len: u64,
31        /// Available payload bytes in memory.
32        available_bytes: u64,
33    },
34}
35
36/// Validate a stable-cell envelope and return its payload length.
37///
38/// Reject empty/foreign memory, unsupported layouts, lengths beyond the backing
39/// extent, and lengths above the caller's `maximum`. No payload is allocated or
40/// read, and memory is never written or grown. This does not validate the value's
41/// codec or schema.
42///
43/// # Panics
44/// Propagates backing `size`/`read` failures.
45pub fn stable_cell_payload_length<M: Memory>(
46    memory: &M,
47    maximum: usize,
48) -> Result<usize, StableCellPayloadError> {
49    if memory.size() == 0 {
50        return Err(StableCellPayloadError::NotStableCell);
51    }
52
53    let mut header = [0; STABLE_CELL_HEADER_SIZE];
54    memory.read(0, &mut header);
55    if &header[..8] != STABLE_CELL_MAGIC || header[12..16].iter().any(|&byte| byte != 0) {
56        return Err(StableCellPayloadError::NotStableCell);
57    }
58    let value_len = u64::from(u32::from_le_bytes(header[8..12].try_into().unwrap()));
59    let available_bytes = memory.size().saturating_mul(WASM_PAGE_SIZE_BYTES);
60    let payload_capacity = available_bytes.saturating_sub(STABLE_CELL_VALUE_OFFSET);
61    if value_len > payload_capacity {
62        return Err(StableCellPayloadError::InvalidLength {
63            value_len,
64            available_bytes: payload_capacity,
65        });
66    }
67    if value_len > maximum as u64 {
68        return Err(StableCellPayloadError::TooLarge { value_len });
69    }
70    #[expect(
71        clippy::cast_possible_truncation,
72        reason = "the admitted caller ceiling is representable as usize"
73    )]
74    let value_len = value_len as usize;
75    Ok(value_len)
76}