Skip to main content

ic_memory/
stable_cell.rs

1use crate::LedgerCommitStore;
2use ic_stable_structures::{Memory, Storable, storable::Bound};
3use serde::{Deserialize, Serialize};
4use std::borrow::Cow;
5use thiserror::Error;
6
7/// Stable-cell magic prefix written by `ic-stable-structures::Cell`.
8pub const STABLE_CELL_MAGIC: &[u8; 3] = b"SCL";
9/// Stable-cell layout version supported by this adapter.
10pub const STABLE_CELL_LAYOUT_VERSION: u8 = 1;
11/// Stable-cell header byte length.
12pub const STABLE_CELL_HEADER_SIZE: usize = 8;
13/// Byte offset where the stable-cell value payload starts.
14pub const STABLE_CELL_VALUE_OFFSET: u64 = 8;
15const WASM_PAGE_SIZE: u64 = 65_536;
16
17///
18/// StableCellLedgerRecord
19///
20/// `ic-stable-structures::Cell` record containing an `ic-memory` allocation
21/// ledger commit store.
22///
23/// This is a substrate adapter DTO. It owns no framework policy and does not
24/// open application allocations.
25///
26
27#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
28pub struct StableCellLedgerRecord {
29    store: LedgerCommitStore,
30}
31
32impl StableCellLedgerRecord {
33    /// Construct a record from a commit store.
34    #[must_use]
35    pub const fn new(store: LedgerCommitStore) -> Self {
36        Self { store }
37    }
38
39    /// Borrow the embedded commit store.
40    #[must_use]
41    pub const fn store(&self) -> &LedgerCommitStore {
42        &self.store
43    }
44
45    /// Mutably borrow the embedded commit store.
46    pub const fn store_mut(&mut self) -> &mut LedgerCommitStore {
47        &mut self.store
48    }
49
50    /// Consume this record and return the embedded commit store.
51    #[must_use]
52    pub fn into_store(self) -> LedgerCommitStore {
53        self.store
54    }
55}
56
57impl Storable for StableCellLedgerRecord {
58    const BOUND: Bound = Bound::Unbounded;
59
60    fn to_bytes(&self) -> Cow<'_, [u8]> {
61        Cow::Owned(serialize_record(self))
62    }
63
64    fn into_bytes(self) -> Vec<u8> {
65        serialize_record(&self)
66    }
67
68    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
69        decode_stable_cell_ledger_record(&bytes).unwrap_or_else(|err| {
70            panic!("StableCellLedgerRecord deserialize failed: {err}");
71        })
72    }
73}
74
75///
76/// StableCellPayloadError
77///
78/// Stable-cell payload decode failure.
79#[derive(Clone, Debug, Eq, Error, PartialEq)]
80pub enum StableCellPayloadError {
81    /// Memory contents do not start with the stable-cell marker.
82    #[error("memory is not an ic-stable-structures Cell")]
83    NotStableCell,
84    /// Stable-cell format version is not supported.
85    #[error("unsupported stable-cell layout version {version}")]
86    UnsupportedVersion {
87        /// Observed stable-cell version.
88        version: u8,
89    },
90    /// Stable-cell header length does not fit inside the memory.
91    #[error("stable-cell payload length {value_len} exceeds available bytes {available_bytes}")]
92    InvalidLength {
93        /// Encoded value length.
94        value_len: u64,
95        /// Available payload bytes in memory.
96        available_bytes: u64,
97    },
98    /// Stable-cell length cannot be represented on the current host.
99    #[error("stable-cell payload length {value_len} cannot fit in usize")]
100    LengthOverflow {
101        /// Encoded value length.
102        value_len: u64,
103    },
104}
105
106/// Decode the raw value payload from an `ic-stable-structures::Cell` memory.
107///
108/// This helper is intentionally narrow: it recognizes the physical stable-cell
109/// envelope and returns the value bytes. It does not deserialize those bytes or
110/// decide whether they represent a valid allocation ledger.
111pub fn decode_stable_cell_payload<M: Memory>(
112    memory: &M,
113) -> Result<Vec<u8>, StableCellPayloadError> {
114    let mut header = [0; STABLE_CELL_HEADER_SIZE];
115    memory.read(0, &mut header);
116    if &header[0..3] != STABLE_CELL_MAGIC {
117        return Err(StableCellPayloadError::NotStableCell);
118    }
119    if header[3] != STABLE_CELL_LAYOUT_VERSION {
120        return Err(StableCellPayloadError::UnsupportedVersion { version: header[3] });
121    }
122
123    let value_len = u64::from(u32::from_le_bytes([
124        header[4], header[5], header[6], header[7],
125    ]));
126    let available_bytes = memory.size().saturating_mul(WASM_PAGE_SIZE);
127    let payload_capacity = available_bytes.saturating_sub(STABLE_CELL_VALUE_OFFSET);
128    if value_len > payload_capacity {
129        return Err(StableCellPayloadError::InvalidLength {
130            value_len,
131            available_bytes: payload_capacity,
132        });
133    }
134    let value_len = usize::try_from(value_len)
135        .map_err(|_| StableCellPayloadError::LengthOverflow { value_len })?;
136
137    let mut bytes = vec![0; value_len];
138    memory.read(STABLE_CELL_VALUE_OFFSET, &mut bytes);
139    Ok(bytes)
140}
141
142/// Decode a `StableCellLedgerRecord` from stable-cell value bytes.
143///
144/// This decodes only the cell value payload, not the enclosing stable-cell
145/// header. Use [`decode_stable_cell_payload`] first when inspecting raw stable
146/// memory.
147pub fn decode_stable_cell_ledger_record(
148    bytes: &[u8],
149) -> Result<StableCellLedgerRecord, serde_cbor::Error> {
150    serde_cbor::from_slice(bytes)
151}
152
153fn serialize_record(record: &StableCellLedgerRecord) -> Vec<u8> {
154    serde_cbor::to_vec(record).unwrap_or_else(|err| {
155        panic!("StableCellLedgerRecord serialize failed: {err}");
156    })
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use ic_stable_structures::{Cell, VectorMemory};
163
164    #[test]
165    fn stable_cell_ledger_record_round_trips_through_cell() {
166        let memory = VectorMemory::default();
167        let record = StableCellLedgerRecord::default();
168        let cell = Cell::init(memory.clone(), record.clone());
169
170        assert_eq!(cell.get(), &record);
171        let payload = decode_stable_cell_payload(&memory).expect("decode stable cell payload");
172        let decoded = StableCellLedgerRecord::from_bytes(Cow::Owned(payload));
173        assert_eq!(decoded, record);
174    }
175
176    #[test]
177    fn stable_cell_payload_rejects_non_cell_memory() {
178        let memory = VectorMemory::default();
179        memory.grow(1);
180        memory.write(0, b"BAD");
181
182        assert_eq!(
183            decode_stable_cell_payload(&memory),
184            Err(StableCellPayloadError::NotStableCell)
185        );
186    }
187}