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