1use crate::{LedgerCommitStore, constants::WASM_PAGE_SIZE_BYTES};
2use ic_platform::stable_structures::{Memory, Storable, storable::Bound};
3use std::borrow::Cow;
4use thiserror::Error;
5
6pub const STABLE_CELL_MAGIC: &[u8; 3] = b"SCL";
8pub const STABLE_CELL_LAYOUT_VERSION: u8 = 1;
10pub const STABLE_CELL_HEADER_SIZE: usize = 8;
12pub const STABLE_CELL_VALUE_OFFSET: u64 = 8;
14
15#[non_exhaustive]
22#[derive(Clone, Debug, Eq, Error, PartialEq)]
23pub enum StableCellPayloadError {
24 #[error("stable-cell ledger payload length {value_len} exceeds recovery limit")]
26 TooLarge { value_len: u64 },
27 #[error("memory is not an ic-stable-structures Cell")]
29 NotStableCell,
30 #[error("unexpected stable-cell layout version {version}")]
32 UnexpectedLayoutVersion {
33 version: u8,
35 },
36 #[error("stable-cell payload length {value_len} exceeds available bytes {available_bytes}")]
38 InvalidLength {
39 value_len: u64,
41 available_bytes: u64,
43 },
44}
45
46#[non_exhaustive]
56#[derive(Debug, Error)]
57pub enum StableCellLedgerError {
58 #[error(transparent)]
60 Payload(#[from] StableCellPayloadError),
61 #[error("stable-cell ledger record decode failed: {0}")]
63 Record(#[source] ic_wire::Error),
64}
65
66pub fn decode_stable_cell_payload<M: Memory>(
72 memory: &M,
73) -> Result<Vec<u8>, StableCellPayloadError> {
74 let value_len = stable_cell_payload_length(memory, crate::constants::MAX_LEDGER_RECORD_BYTES)?;
75 let mut bytes = vec![0; value_len];
76 memory.read(STABLE_CELL_VALUE_OFFSET, &mut bytes);
77 Ok(bytes)
78}
79
80pub fn stable_cell_payload_length<M: Memory>(
90 memory: &M,
91 maximum: usize,
92) -> Result<usize, StableCellPayloadError> {
93 if memory.size() == 0 {
94 return Err(StableCellPayloadError::NotStableCell);
95 }
96
97 let mut header = [0; STABLE_CELL_HEADER_SIZE];
98 memory.read(0, &mut header);
99 if &header[0..3] != STABLE_CELL_MAGIC {
100 return Err(StableCellPayloadError::NotStableCell);
101 }
102 if header[3] != STABLE_CELL_LAYOUT_VERSION {
103 return Err(StableCellPayloadError::UnexpectedLayoutVersion { version: header[3] });
104 }
105
106 let value_len = u64::from(u32::from_le_bytes([
107 header[4], header[5], header[6], header[7],
108 ]));
109 let available_bytes = memory.size().saturating_mul(WASM_PAGE_SIZE_BYTES);
110 let payload_capacity = available_bytes.saturating_sub(STABLE_CELL_VALUE_OFFSET);
111 if value_len > payload_capacity {
112 return Err(StableCellPayloadError::InvalidLength {
113 value_len,
114 available_bytes: payload_capacity,
115 });
116 }
117 if value_len > maximum as u64 {
118 return Err(StableCellPayloadError::TooLarge { value_len });
119 }
120 #[expect(
121 clippy::cast_possible_truncation,
122 reason = "the admitted caller ceiling is representable as usize"
123 )]
124 let value_len = value_len as usize;
125 Ok(value_len)
126}
127
128pub fn decode_stable_cell_commit_store(bytes: &[u8]) -> Result<LedgerCommitStore, ic_wire::Error> {
137 use crate::encoding::{RECORD_LIMITS, RECORD_MAGIC};
138 if bytes.len() > crate::constants::MAX_LEDGER_RECORD_BYTES {
139 return Err(ic_wire::Error::Limit);
140 }
141 let payload = bytes
142 .strip_prefix(RECORD_MAGIC)
143 .ok_or(ic_wire::Error::Malformed)?;
144 ic_wire::native::decode(payload, RECORD_LIMITS)
145}
146
147pub fn decode_stable_cell_commit_store_from_memory<M: Memory>(
155 memory: &M,
156) -> Result<LedgerCommitStore, StableCellLedgerError> {
157 if memory.size() == 0 {
158 return Ok(LedgerCommitStore::default());
159 }
160
161 let payload = decode_stable_cell_payload(memory)?;
162 decode_stable_cell_commit_store(&payload).map_err(StableCellLedgerError::Record)
163}
164
165pub struct EncodedLedgerRecord {
172 bytes: Vec<u8>,
173}
174
175impl EncodedLedgerRecord {
176 pub(crate) fn new(record: &LedgerCommitStore) -> Result<Self, ic_wire::Error> {
177 use crate::encoding::{RECORD_LIMITS, RECORD_MAGIC};
178 let payload = ic_wire::native::encode(record, RECORD_LIMITS)?;
179 let mut bytes = Vec::with_capacity(RECORD_MAGIC.len() + payload.len());
180 bytes.extend_from_slice(RECORD_MAGIC);
181 bytes.extend_from_slice(&payload);
182 Ok(Self { bytes })
183 }
184
185 pub(crate) const fn len(&self) -> usize {
186 self.bytes.len()
187 }
188}
189
190impl Storable for EncodedLedgerRecord {
191 const BOUND: Bound = Bound::Unbounded;
192
193 fn to_bytes(&self) -> Cow<'_, [u8]> {
194 Cow::Borrowed(&self.bytes)
195 }
196 fn into_bytes(self) -> Vec<u8> {
197 self.bytes
198 }
199 fn from_bytes(_: Cow<'_, [u8]>) -> Self {
200 panic!("publication bytes cannot recover ledger authority")
201 }
202}
203
204#[cfg(test)]
205mod tests;