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::with_capacity(length);
92    if length != 0 {
93        // SAFETY: Fresh capacity provides exclusive, correctly aligned storage
94        // for at least `length` bytes, disjoint from the backing and its state.
95        // A successful read initializes the entire destination before set_len
96        // publishes it. If reading unwinds after a partial copy, the Vec still
97        // has length zero and drops without exposing uninitialized bytes.
98        #[expect(
99            unsafe_code,
100            reason = "The admitted read initializes fresh Vec capacity."
101        )]
102        unsafe {
103            memory.read_unsafe(PAYLOAD_OFFSET, bytes.as_mut_ptr(), length);
104            bytes.set_len(length);
105        }
106    }
107    bytes
108}
109
110pub fn write_payload<M: Memory>(memory: &M, bytes: &[u8], previous_length: Option<usize>) -> bool {
111    let Ok(length) = u64::try_from(bytes.len()) else {
112        return false;
113    };
114    let Some(end) = PAYLOAD_OFFSET.checked_add(length) else {
115        return false;
116    };
117    // The exclusive lease keeps the previously admitted extent valid. A
118    // replacement inside that extent requires neither reservation nor a query.
119    if previous_length.is_none_or(|previous| bytes.len() > previous) {
120        let needed_pages = end.div_ceil(WASM_PAGE_SIZE_BYTES);
121        if needed_pages.checked_mul(WASM_PAGE_SIZE_BYTES).is_none() {
122            return false;
123        }
124        let current_pages = memory.size();
125        if current_pages.checked_mul(WASM_PAGE_SIZE_BYTES).is_none() {
126            return false;
127        }
128        if needed_pages > current_pages {
129            if memory.grow(needed_pages - current_pages) < 0 {
130                return false;
131            }
132            // Check a successful growth before publishing any bytes.
133            if memory
134                .size()
135                .checked_mul(WASM_PAGE_SIZE_BYTES)
136                .is_none_or(|capacity| capacity < end)
137            {
138                return false;
139            }
140        }
141    }
142    if !bytes.is_empty() {
143        memory.write(PAYLOAD_OFFSET, bytes);
144    }
145    // Publishing the header last does not promise native atomicity. The owner
146    // health guard covers every write; IC executions supply their own rollback.
147    if previous_length != Some(bytes.len()) {
148        memory.write(0, &header(length));
149    }
150    true
151}
152
153#[cfg(test)]
154mod tests;