Skip to main content

ic_storage/adapter/
cell.rs

1//! Governed single-value storage over an exclusive allocation lease.
2use super::{
3    Bound, Memory, Storable,
4    error::{CollectionError as Error, Result, ensure},
5};
6use crate::stable_cell::{STABLE_CELL_MAGIC, STABLE_CELL_VALUE_OFFSET};
7use crate::{
8    DefaultMemoryImpl, RuntimeMemory, STABLE_CELL_HEADER_SIZE, WASM_PAGE_SIZE_BYTES, hash,
9};
10use std::borrow::Cow;
11
12///
13/// `StableCell`
14///
15/// A cached, checksummed value persisted in an exclusively leased allocation.
16/// Loading is read-only and requires a caller-selected payload byte ceiling.
17///
18pub struct StableCell<T: Storable, M: Memory = DefaultMemoryImpl> {
19    memory: RuntimeMemory<M>,
20    value: T,
21}
22
23impl<T: Storable, M: Memory> StableCell<T, M> {
24    /// Create in empty memory, admitting encoding and bounds before writes.
25    ///
26    /// # Errors
27    /// Rejects nonempty memory, invalid encoding/bounds or capacity refusal before writing.
28    pub fn create(memory: RuntimeMemory<M>, value: T) -> Result<Self> {
29        ensure(memory.size() == 0, Error::NotEmpty)?;
30        Self::reset(memory, value)
31    }
32
33    /// Explicitly replace contents. Backing failures retain host rollback semantics.
34    ///
35    /// # Errors
36    /// Rejects invalid encoding/bounds or capacity refusal before replacing contents.
37    pub fn reset(memory: RuntimeMemory<M>, value: T) -> Result<Self> {
38        Self::write(&memory, &value)?;
39        Ok(Self { memory, value })
40    }
41
42    /// Load without writing. Admit the smaller of `maximum` and the codec bound
43    /// before allocating, then verify integrity before decoding application data.
44    ///
45    /// # Errors
46    /// Rejects foreign, corrupt or oversized records and incompatible codec bounds.
47    ///
48    /// # Panics
49    /// Propagates backing panics or a faulty caller codec; no native rollback is provided.
50    pub fn load(memory: RuntimeMemory<M>, maximum: usize) -> Result<Self> {
51        let maximum = match T::BOUND {
52            Bound::Bounded { max_size, .. } => maximum.min(max_size as usize),
53            Bound::Unbounded => maximum,
54        };
55        let length = crate::stable_cell_payload_length(&memory, maximum)?;
56        let mut header = [0; STABLE_CELL_HEADER_SIZE];
57        memory.read(0, &mut header);
58        let mut bytes = vec![0; length];
59        memory.read(STABLE_CELL_VALUE_OFFSET, &mut bytes);
60        let expected = u64::from_le_bytes(header[16..24].try_into().unwrap());
61        ensure(checksum(&header, &bytes) == expected, Error::Checksum)?;
62        T::check_bounds(&bytes)?;
63        let value = T::from_bytes(Cow::Owned(bytes))?;
64        Ok(Self { memory, value })
65    }
66
67    /// Borrow the cached value without IO or decoding.
68    pub const fn get(&self) -> &T {
69        &self.value
70    }
71
72    /// Persist a replacement and return the old value. Admission and growth
73    /// refusal preserve existing bytes. Native interrupted writes are not atomic.
74    ///
75    /// # Errors
76    /// Rejects invalid indices, encoding, bounds or capacity refusal before writes.
77    pub fn set(&mut self, value: T) -> Result<T> {
78        Self::write(&self.memory, &value)?;
79        Ok(std::mem::replace(&mut self.value, value))
80    }
81
82    fn write(memory: &RuntimeMemory<M>, value: &T) -> Result<()> {
83        let bytes = value.to_bytes_checked()?;
84        let length = u32::try_from(bytes.len()).map_err(|_| Error::Budget)?;
85        let pages = (STABLE_CELL_VALUE_OFFSET + u64::from(length)).div_ceil(WASM_PAGE_SIZE_BYTES);
86        let current = memory.size();
87        if pages > current {
88            memory.grow(pages - current).map_err(|_| Error::Growth)?;
89        }
90        let mut header = [0; STABLE_CELL_HEADER_SIZE];
91        header[..8].copy_from_slice(STABLE_CELL_MAGIC);
92        header[8..12].copy_from_slice(&length.to_le_bytes());
93        let digest = checksum(&header, &bytes);
94        header[16..24].copy_from_slice(&digest.to_le_bytes());
95        memory.write(STABLE_CELL_VALUE_OFFSET, &bytes);
96        memory.write(0, &header);
97        Ok(())
98    }
99}
100
101fn checksum(header: &[u8], payload: &[u8]) -> u64 {
102    hash::fnv64(hash::fnv64(hash::FNV_OFFSET, &header[..16]), payload)
103}