1use crate::{LedgerCommitStore, constants::WASM_PAGE_SIZE_BYTES};
2use ic_stable_structures::{Memory, Storable, storable::Bound};
3use serde::{Deserialize, Serialize};
4use std::borrow::Cow;
5use thiserror::Error;
6
7pub const STABLE_CELL_MAGIC: &[u8; 3] = b"SCL";
9pub const STABLE_CELL_LAYOUT_VERSION: u8 = 1;
11pub const STABLE_CELL_HEADER_SIZE: usize = 8;
13pub const STABLE_CELL_VALUE_OFFSET: u64 = 8;
15
16#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
27#[serde(deny_unknown_fields)]
28pub struct StableCellLedgerRecord {
29 store: LedgerCommitStore,
30}
31
32impl StableCellLedgerRecord {
33 #[must_use]
35 pub const fn new(store: LedgerCommitStore) -> Self {
36 Self { store }
37 }
38
39 #[must_use]
41 pub const fn store(&self) -> &LedgerCommitStore {
42 &self.store
43 }
44
45 pub const fn store_mut(&mut self) -> &mut LedgerCommitStore {
47 &mut self.store
48 }
49
50 #[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#[derive(Clone, Debug, Eq, Error, PartialEq)]
80pub enum StableCellPayloadError {
81 #[error("memory is not an ic-stable-structures Cell")]
83 NotStableCell,
84 #[error("unexpected stable-cell layout version {version}")]
86 UnexpectedLayoutVersion {
87 version: u8,
89 },
90 #[error("stable-cell payload length {value_len} exceeds available bytes {available_bytes}")]
92 InvalidLength {
93 value_len: u64,
95 available_bytes: u64,
97 },
98 #[error("stable-cell payload length {value_len} cannot fit in usize")]
100 LengthOverflow {
101 value_len: u64,
103 },
104}
105
106#[derive(Debug, Error)]
111pub enum StableCellLedgerError {
112 #[error(transparent)]
114 Payload(#[from] StableCellPayloadError),
115 #[error("stable-cell ledger record decode failed")]
117 Record(#[source] serde_cbor::Error),
118}
119
120pub fn decode_stable_cell_payload<M: Memory>(
126 memory: &M,
127) -> Result<Vec<u8>, StableCellPayloadError> {
128 if memory.size() == 0 {
129 return Err(StableCellPayloadError::NotStableCell);
130 }
131
132 let mut header = [0; STABLE_CELL_HEADER_SIZE];
133 memory.read(0, &mut header);
134 if &header[0..3] != STABLE_CELL_MAGIC {
135 return Err(StableCellPayloadError::NotStableCell);
136 }
137 if header[3] != STABLE_CELL_LAYOUT_VERSION {
138 return Err(StableCellPayloadError::UnexpectedLayoutVersion { version: header[3] });
139 }
140
141 let value_len = u64::from(u32::from_le_bytes([
142 header[4], header[5], header[6], header[7],
143 ]));
144 let available_bytes = memory.size().saturating_mul(WASM_PAGE_SIZE_BYTES);
145 let payload_capacity = available_bytes.saturating_sub(STABLE_CELL_VALUE_OFFSET);
146 if value_len > payload_capacity {
147 return Err(StableCellPayloadError::InvalidLength {
148 value_len,
149 available_bytes: payload_capacity,
150 });
151 }
152 let value_len = usize::try_from(value_len)
153 .map_err(|_| StableCellPayloadError::LengthOverflow { value_len })?;
154
155 let mut bytes = vec![0; value_len];
156 memory.read(STABLE_CELL_VALUE_OFFSET, &mut bytes);
157 Ok(bytes)
158}
159
160pub fn decode_stable_cell_ledger_record(
169 bytes: &[u8],
170) -> Result<StableCellLedgerRecord, serde_cbor::Error> {
171 serde_cbor::from_slice(bytes)
172}
173
174pub fn validate_stable_cell_ledger_memory<M: Memory>(
182 memory: &M,
183) -> Result<(), StableCellLedgerError> {
184 if memory.size() == 0 {
185 return Ok(());
186 }
187
188 let payload = decode_stable_cell_payload(memory)?;
189 decode_stable_cell_ledger_record(&payload).map_err(StableCellLedgerError::Record)?;
190 Ok(())
191}
192
193fn serialize_record(record: &StableCellLedgerRecord) -> Vec<u8> {
194 serde_cbor::to_vec(record).unwrap_or_else(|err| {
195 panic!("StableCellLedgerRecord serialize failed: {err}");
196 })
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use ic_stable_structures::{Cell, VectorMemory};
203
204 fn hex_fixture(contents: &str) -> Vec<u8> {
205 let hex = contents
206 .chars()
207 .filter(|char| !char.is_whitespace())
208 .collect::<String>();
209 assert_eq!(hex.len() % 2, 0, "fixture hex must have byte pairs");
210 hex.as_bytes()
211 .chunks_exact(2)
212 .map(|pair| {
213 let pair = std::str::from_utf8(pair).expect("fixture hex is utf8");
214 u8::from_str_radix(pair, 16).expect("fixture hex byte")
215 })
216 .collect()
217 }
218
219 #[test]
220 fn stable_cell_ledger_record_round_trips_through_cell() {
221 let memory = VectorMemory::default();
222 let record = StableCellLedgerRecord::default();
223 let cell = Cell::init(memory.clone(), record.clone());
224
225 assert_eq!(cell.get(), &record);
226 let payload = decode_stable_cell_payload(&memory).expect("decode stable cell payload");
227 let decoded = StableCellLedgerRecord::from_bytes(Cow::Owned(payload));
228 assert_eq!(decoded, record);
229 }
230
231 #[test]
232 fn current_stable_cell_record_fixture_recovers() {
233 let bytes = hex_fixture(include_str!(
234 "../fixtures/current/stable_cell_record.cbor.hex"
235 ));
236 let record = decode_stable_cell_ledger_record(&bytes).expect("stable-cell fixture");
237
238 assert_eq!(
239 bytes,
240 serde_cbor::to_vec(&record).expect("re-encoded stable-cell fixture")
241 );
242 assert_eq!(
243 record
244 .store()
245 .recover()
246 .expect("fixture store recovers")
247 .current_generation(),
248 1
249 );
250 }
251
252 #[test]
253 fn stable_cell_ledger_record_rejects_unknown_top_level_fields() {
254 use serde_cbor::Value;
255 use std::collections::BTreeMap;
256
257 let mut map = BTreeMap::new();
258 map.insert(
259 Value::Text("store".to_string()),
260 serde_cbor::value::to_value(LedgerCommitStore::default()).expect("store value"),
261 );
262 map.insert(Value::Text("future_field".to_string()), Value::Bool(true));
263 let bytes = serde_cbor::to_vec(&Value::Map(map)).expect("unknown-field stable cell");
264
265 let err = decode_stable_cell_ledger_record(&bytes)
266 .expect_err("unknown stable-cell record field must fail closed");
267
268 assert!(err.to_string().contains("future_field"));
269 }
270
271 #[test]
272 fn stable_cell_payload_rejects_non_cell_memory() {
273 let memory = VectorMemory::default();
274 memory.grow(1);
275 memory.write(0, b"BAD");
276
277 assert_eq!(
278 decode_stable_cell_payload(&memory),
279 Err(StableCellPayloadError::NotStableCell)
280 );
281 }
282
283 #[test]
284 fn stable_cell_payload_rejects_empty_memory_without_panic() {
285 let memory = VectorMemory::default();
286
287 assert_eq!(
288 decode_stable_cell_payload(&memory),
289 Err(StableCellPayloadError::NotStableCell)
290 );
291 }
292
293 #[test]
294 fn stable_cell_ledger_preflight_classifies_bad_record_without_panic() {
295 let memory = VectorMemory::default();
296 memory.grow(1);
297 memory.write(0, STABLE_CELL_MAGIC);
298 memory.write(3, &[STABLE_CELL_LAYOUT_VERSION]);
299 memory.write(4, &1_u32.to_le_bytes());
300 memory.write(STABLE_CELL_VALUE_OFFSET, &[0xff]);
301
302 let err =
303 validate_stable_cell_ledger_memory(&memory).expect_err("bad record must be classified");
304
305 assert!(matches!(err, StableCellLedgerError::Record(_)));
306 }
307}