use crate::{Memory, WASM_PAGE_SIZE_BYTES};
pub const STABLE_CELL_HEADER_SIZE: usize = 24;
const DISCRIMINATOR: [u8; 8] = *b"STORCELL";
const VERSION: u32 = 1;
const PAYLOAD_OFFSET: u64 = STABLE_CELL_HEADER_SIZE as u64;
#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum StableCellPayloadError {
#[error("Cell payload length {value_len} exceeds the admitted limit")]
TooLarge { value_len: u64 },
#[error("allocation does not contain supported Cell framing")]
NotStableCell,
#[error("Cell payload length {value_len} exceeds available bytes {available_bytes}")]
InvalidLength {
value_len: u64,
available_bytes: u64,
},
}
pub fn stable_cell_payload_length<M: Memory>(
memory: &M,
maximum: usize,
) -> Result<usize, StableCellPayloadError> {
let capacity = memory
.size()
.checked_mul(WASM_PAGE_SIZE_BYTES)
.filter(|capacity| *capacity >= PAYLOAD_OFFSET)
.ok_or(StableCellPayloadError::NotStableCell)?;
let mut header = [0; STABLE_CELL_HEADER_SIZE];
memory.read(0, &mut header);
if header[..8] != DISCRIMINATOR
|| header[8..12] != VERSION.to_le_bytes()
|| header[12..16] != [0; 4]
{
return Err(StableCellPayloadError::NotStableCell);
}
let mut length = [0; 8];
length.copy_from_slice(&header[16..]);
let value_len = u64::from_le_bytes(length);
let available_bytes = capacity - PAYLOAD_OFFSET;
if value_len > available_bytes {
return Err(StableCellPayloadError::InvalidLength {
value_len,
available_bytes,
});
}
let length =
usize::try_from(value_len).map_err(|_| StableCellPayloadError::TooLarge { value_len })?;
if length > maximum || length > isize::MAX as usize {
return Err(StableCellPayloadError::TooLarge { value_len });
}
Ok(length)
}
fn header(value_len: u64) -> [u8; STABLE_CELL_HEADER_SIZE] {
let mut bytes = [0; STABLE_CELL_HEADER_SIZE];
bytes[..8].copy_from_slice(&DISCRIMINATOR);
bytes[8..12].copy_from_slice(&VERSION.to_le_bytes());
bytes[16..].copy_from_slice(&value_len.to_le_bytes());
bytes
}
pub fn read_payload<M: Memory>(memory: &M, length: usize) -> Vec<u8> {
let mut bytes = Vec::with_capacity(length);
if length != 0 {
#[expect(
unsafe_code,
reason = "The admitted read initializes fresh Vec capacity."
)]
unsafe {
memory.read_unsafe(PAYLOAD_OFFSET, bytes.as_mut_ptr(), length);
bytes.set_len(length);
}
}
bytes
}
pub fn write_payload<M: Memory>(memory: &M, bytes: &[u8], previous_length: Option<usize>) -> bool {
let Ok(length) = u64::try_from(bytes.len()) else {
return false;
};
let Some(end) = PAYLOAD_OFFSET.checked_add(length) else {
return false;
};
if previous_length.is_none_or(|previous| bytes.len() > previous) {
let needed_pages = end.div_ceil(WASM_PAGE_SIZE_BYTES);
if needed_pages.checked_mul(WASM_PAGE_SIZE_BYTES).is_none() {
return false;
}
let current_pages = memory.size();
if current_pages.checked_mul(WASM_PAGE_SIZE_BYTES).is_none() {
return false;
}
if needed_pages > current_pages {
if memory.grow(needed_pages - current_pages) < 0 {
return false;
}
if memory
.size()
.checked_mul(WASM_PAGE_SIZE_BYTES)
.is_none_or(|capacity| capacity < end)
{
return false;
}
}
}
if !bytes.is_empty() {
memory.write(PAYLOAD_OFFSET, bytes);
}
if previous_length != Some(bytes.len()) {
memory.write(0, &header(length));
}
true
}
#[cfg(test)]
mod tests;