Skip to main content

ic_storage/
stable_cell.rs

1//! Cell framing and read-only payload admission.
2//!
3//! This module owns the persisted envelope, not application codecs or leases.
4
5use crate::{Memory, WASM_PAGE_SIZE_BYTES};
6
7/// Number of bytes preceding a Cell payload in the current format.
8pub const STABLE_CELL_HEADER_SIZE: usize = 24;
9
10const DISCRIMINATOR: [u8; 8] = *b"STORCELL";
11const VERSION: u32 = 1;
12const PAYLOAD_OFFSET: u64 = STABLE_CELL_HEADER_SIZE as u64;
13
14///
15/// `StableCellPayloadError`
16///
17/// A framing or size refusal detected before reading the application payload.
18///
19
20#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
21#[non_exhaustive]
22pub enum StableCellPayloadError {
23    /// The payload exceeds the caller's ceiling or the host's buffer limit.
24    #[error("Cell payload length {value_len} exceeds the admitted limit")]
25    TooLarge { value_len: u64 },
26    /// The allocation does not contain supported Cell framing.
27    #[error("allocation does not contain supported Cell framing")]
28    NotStableCell,
29    /// The declared payload extends beyond the allocation.
30    #[error("Cell payload length {value_len} exceeds available bytes {available_bytes}")]
31    InvalidLength {
32        value_len: u64,
33        available_bytes: u64,
34    },
35}
36
37/// Admit the current Cell framing and return its payload length without reading
38/// or decoding the payload.
39///
40/// # Errors
41/// Returns an error for unsupported framing, an impossible extent, or a payload
42/// above `maximum` or the host's addressable buffer limit.
43///
44/// # Panics
45/// Panics if the supplied memory implementation refuses a size query or read.
46pub fn stable_cell_payload_length<M: Memory>(
47    memory: &M,
48    maximum: usize,
49) -> Result<usize, StableCellPayloadError> {
50    let capacity = memory
51        .size()
52        .checked_mul(WASM_PAGE_SIZE_BYTES)
53        .filter(|capacity| *capacity >= PAYLOAD_OFFSET)
54        .ok_or(StableCellPayloadError::NotStableCell)?;
55    let mut header = [0; STABLE_CELL_HEADER_SIZE];
56    memory.read(0, &mut header);
57    if header[..8] != DISCRIMINATOR
58        || header[8..12] != VERSION.to_le_bytes()
59        || header[12..16] != [0; 4]
60    {
61        return Err(StableCellPayloadError::NotStableCell);
62    }
63    let mut length = [0; 8];
64    length.copy_from_slice(&header[16..]);
65    let value_len = u64::from_le_bytes(length);
66    let available_bytes = capacity - PAYLOAD_OFFSET;
67    if value_len > available_bytes {
68        return Err(StableCellPayloadError::InvalidLength {
69            value_len,
70            available_bytes,
71        });
72    }
73    let length =
74        usize::try_from(value_len).map_err(|_| StableCellPayloadError::TooLarge { value_len })?;
75    if length > maximum || length > isize::MAX as usize {
76        return Err(StableCellPayloadError::TooLarge { value_len });
77    }
78    Ok(length)
79}
80
81fn header(value_len: u64) -> [u8; STABLE_CELL_HEADER_SIZE] {
82    let mut bytes = [0; STABLE_CELL_HEADER_SIZE];
83    bytes[..8].copy_from_slice(&DISCRIMINATOR);
84    bytes[8..12].copy_from_slice(&VERSION.to_le_bytes());
85    bytes[16..].copy_from_slice(&value_len.to_le_bytes());
86    bytes
87}
88
89pub fn read_payload<M: Memory>(memory: &M, length: usize) -> Vec<u8> {
90    // The caller has admitted this complete extent before allocating the buffer.
91    let mut bytes = vec![0; length];
92    if !bytes.is_empty() {
93        memory.read(PAYLOAD_OFFSET, &mut bytes);
94    }
95    bytes
96}
97
98pub fn write_payload<M: Memory>(memory: &M, bytes: &[u8], previous_length: Option<usize>) -> bool {
99    let Ok(length) = u64::try_from(bytes.len()) else {
100        return false;
101    };
102    let Some(end) = PAYLOAD_OFFSET.checked_add(length) else {
103        return false;
104    };
105    // The exclusive lease keeps the previously admitted extent valid. A
106    // replacement inside that extent requires neither reservation nor a query.
107    if previous_length.is_none_or(|previous| bytes.len() > previous) {
108        let needed_pages = end.div_ceil(WASM_PAGE_SIZE_BYTES);
109        if needed_pages.checked_mul(WASM_PAGE_SIZE_BYTES).is_none() {
110            return false;
111        }
112        let current_pages = memory.size();
113        if current_pages.checked_mul(WASM_PAGE_SIZE_BYTES).is_none() {
114            return false;
115        }
116        if needed_pages > current_pages {
117            if memory.grow(needed_pages - current_pages) < 0 {
118                return false;
119            }
120            // Check a successful growth before publishing any bytes.
121            if memory
122                .size()
123                .checked_mul(WASM_PAGE_SIZE_BYTES)
124                .is_none_or(|capacity| capacity < end)
125            {
126                return false;
127            }
128        }
129    }
130    if !bytes.is_empty() {
131        memory.write(PAYLOAD_OFFSET, bytes);
132    }
133    // Publishing the header last does not promise native atomicity. The owner
134    // health guard covers every write; IC executions supply their own rollback.
135    if previous_length != Some(bytes.len()) {
136        memory.write(0, &header(length));
137    }
138    true
139}
140
141#[cfg(test)]
142mod tests;