use crate::LedgerCommitStore;
use ic_stable_structures::{Memory, Storable, storable::Bound};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use thiserror::Error;
pub const STABLE_CELL_MAGIC: &[u8; 3] = b"SCL";
pub const STABLE_CELL_LAYOUT_VERSION: u8 = 1;
pub const STABLE_CELL_HEADER_SIZE: usize = 8;
pub const STABLE_CELL_VALUE_OFFSET: u64 = 8;
const WASM_PAGE_SIZE: u64 = 65_536;
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct StableCellLedgerRecord {
store: LedgerCommitStore,
}
impl StableCellLedgerRecord {
#[must_use]
pub const fn new(store: LedgerCommitStore) -> Self {
Self { store }
}
#[must_use]
pub const fn store(&self) -> &LedgerCommitStore {
&self.store
}
pub const fn store_mut(&mut self) -> &mut LedgerCommitStore {
&mut self.store
}
#[must_use]
pub fn into_store(self) -> LedgerCommitStore {
self.store
}
}
impl Storable for StableCellLedgerRecord {
const BOUND: Bound = Bound::Unbounded;
fn to_bytes(&self) -> Cow<'_, [u8]> {
Cow::Owned(serialize_record(self))
}
fn into_bytes(self) -> Vec<u8> {
serialize_record(&self)
}
fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
decode_stable_cell_ledger_record(&bytes).unwrap_or_else(|err| {
panic!("StableCellLedgerRecord deserialize failed: {err}");
})
}
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
pub enum StableCellPayloadError {
#[error("memory is not an ic-stable-structures Cell")]
NotStableCell,
#[error("unsupported stable-cell layout version {version}")]
UnsupportedVersion {
version: u8,
},
#[error("stable-cell payload length {value_len} exceeds available bytes {available_bytes}")]
InvalidLength {
value_len: u64,
available_bytes: u64,
},
#[error("stable-cell payload length {value_len} cannot fit in usize")]
LengthOverflow {
value_len: u64,
},
}
#[derive(Debug, Error)]
pub enum StableCellLedgerError {
#[error(transparent)]
Payload(#[from] StableCellPayloadError),
#[error("stable-cell ledger record decode failed")]
Record(#[source] serde_cbor::Error),
}
pub fn decode_stable_cell_payload<M: Memory>(
memory: &M,
) -> Result<Vec<u8>, StableCellPayloadError> {
let mut header = [0; STABLE_CELL_HEADER_SIZE];
memory.read(0, &mut header);
if &header[0..3] != STABLE_CELL_MAGIC {
return Err(StableCellPayloadError::NotStableCell);
}
if header[3] != STABLE_CELL_LAYOUT_VERSION {
return Err(StableCellPayloadError::UnsupportedVersion { version: header[3] });
}
let value_len = u64::from(u32::from_le_bytes([
header[4], header[5], header[6], header[7],
]));
let available_bytes = memory.size().saturating_mul(WASM_PAGE_SIZE);
let payload_capacity = available_bytes.saturating_sub(STABLE_CELL_VALUE_OFFSET);
if value_len > payload_capacity {
return Err(StableCellPayloadError::InvalidLength {
value_len,
available_bytes: payload_capacity,
});
}
let value_len = usize::try_from(value_len)
.map_err(|_| StableCellPayloadError::LengthOverflow { value_len })?;
let mut bytes = vec![0; value_len];
memory.read(STABLE_CELL_VALUE_OFFSET, &mut bytes);
Ok(bytes)
}
pub fn decode_stable_cell_ledger_record(
bytes: &[u8],
) -> Result<StableCellLedgerRecord, serde_cbor::Error> {
serde_cbor::from_slice(bytes)
}
pub fn validate_stable_cell_ledger_memory<M: Memory>(
memory: &M,
) -> Result<(), StableCellLedgerError> {
if memory.size() == 0 {
return Ok(());
}
let payload = decode_stable_cell_payload(memory)?;
decode_stable_cell_ledger_record(&payload).map_err(StableCellLedgerError::Record)?;
Ok(())
}
fn serialize_record(record: &StableCellLedgerRecord) -> Vec<u8> {
serde_cbor::to_vec(record).unwrap_or_else(|err| {
panic!("StableCellLedgerRecord serialize failed: {err}");
})
}
#[cfg(test)]
mod tests {
use super::*;
use ic_stable_structures::{Cell, VectorMemory};
fn hex_fixture(contents: &str) -> Vec<u8> {
let hex = contents
.chars()
.filter(|char| !char.is_whitespace())
.collect::<String>();
assert_eq!(hex.len() % 2, 0, "fixture hex must have byte pairs");
hex.as_bytes()
.chunks_exact(2)
.map(|pair| {
let pair = std::str::from_utf8(pair).expect("fixture hex is utf8");
u8::from_str_radix(pair, 16).expect("fixture hex byte")
})
.collect()
}
#[test]
fn stable_cell_ledger_record_round_trips_through_cell() {
let memory = VectorMemory::default();
let record = StableCellLedgerRecord::default();
let cell = Cell::init(memory.clone(), record.clone());
assert_eq!(cell.get(), &record);
let payload = decode_stable_cell_payload(&memory).expect("decode stable cell payload");
let decoded = StableCellLedgerRecord::from_bytes(Cow::Owned(payload));
assert_eq!(decoded, record);
}
#[test]
fn v1_stable_cell_record_fixture_recovers() {
let bytes = hex_fixture(include_str!("../fixtures/v1/stable_cell_record.cbor.hex"));
let record = decode_stable_cell_ledger_record(&bytes).expect("stable-cell fixture");
assert_eq!(
bytes,
serde_cbor::to_vec(&record).expect("re-encoded stable-cell fixture")
);
assert_eq!(
record
.store()
.recover()
.expect("fixture store recovers")
.current_generation(),
1
);
}
#[test]
fn stable_cell_payload_rejects_non_cell_memory() {
let memory = VectorMemory::default();
memory.grow(1);
memory.write(0, b"BAD");
assert_eq!(
decode_stable_cell_payload(&memory),
Err(StableCellPayloadError::NotStableCell)
);
}
#[test]
fn stable_cell_ledger_preflight_classifies_bad_record_without_panic() {
let memory = VectorMemory::default();
memory.grow(1);
memory.write(0, STABLE_CELL_MAGIC);
memory.write(3, &[STABLE_CELL_LAYOUT_VERSION]);
memory.write(4, &1_u32.to_le_bytes());
memory.write(STABLE_CELL_VALUE_OFFSET, &[0xff]);
let err =
validate_stable_cell_ledger_memory(&memory).expect_err("bad record must be classified");
assert!(matches!(err, StableCellLedgerError::Record(_)));
}
}