ic-memory 0.48.2

Durable stable-memory allocation governance for Internet Computer canisters
Documentation
//! Explicit fixture-only ledger and cell setup; callers own codecs, bounds and lifecycle calls.

pub use crate::runtime::fixture_diagnostics::{
    DiagnosticCheck, DiagnosticCode, DiagnosticFailure, DiagnosticRuntimeBinding,
    DiagnosticStableCell, DiagnosticStableCellStatus, MemoryRuntimeDoctorReport, doctor_report,
};

mod sparse_image;
pub use sparse_image::{SparseMemoryImageError, memory_from_sparse_hex};

mod runtime;
pub use runtime::{FixtureRuntimeError, fixture_runtime};

use crate::{Memory, MemoryManagerConfig, RuntimeMemory};
use ic_stable_structures::Cell;

use crate::{
    AllocationDeclaration, MemoryManagerSlot, MemoryManagerSlotError, StableKey, StableKeyError,
};

///
/// DeclarationSnapshotError
///
/// Checked allocation identity construction failure.
///

#[non_exhaustive]
#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
pub enum DeclarationSnapshotError {
    /// Stable-key grammar failure.
    #[error(transparent)]
    Key(StableKeyError),
    /// `MemoryManager` slot validation failure.
    #[error(transparent)]
    MemoryManagerSlot(MemoryManagerSlotError),
}

impl AllocationDeclaration {
    /// Build a declaration from raw parts after validating allocation identity.
    pub fn new(
        stable_key: impl AsRef<str>,
        slot: MemoryManagerSlot,
    ) -> Result<Self, DeclarationSnapshotError> {
        let stable_key = StableKey::parse(stable_key).map_err(DeclarationSnapshotError::Key)?;
        Ok(Self { stable_key, slot })
    }

    /// Build a checked `MemoryManager` declaration with a usable slot.
    pub fn memory_manager(
        stable_key: impl AsRef<str>,
        id: u8,
    ) -> Result<Self, DeclarationSnapshotError> {
        let slot =
            MemoryManagerSlot::new(id).map_err(DeclarationSnapshotError::MemoryManagerSlot)?;
        Self::new(stable_key, slot)
    }
}

/// Passive fixture DTOs and bounded raw snapshot readers. These do not recover
/// allocation authority or grant memory access; production callers use the runtime.
pub use crate::ledger::{AllocationLedger, LedgerCommitStore};
pub use crate::stable_cell::{
    decode_stable_cell_commit_store, decode_stable_cell_commit_store_from_memory,
};

/// Construct a passive fixture binding from checked declaration fields.
///
/// This performs no transition, generation advance, persistence or capability
/// publication. Compose records with [`AllocationLedger::new`] to check
/// ledger-wide uniqueness and genesis rules before encoding fixture bytes.
#[must_use]
pub fn allocation_record(declaration: &crate::AllocationDeclaration) -> crate::AllocationRecord {
    crate::AllocationRecord::from_declaration(declaration)
}

///
/// FixtureCell
///
/// One byte cell opened through an allocation runtime only by an explicit
/// caller. Namespace grants and the stable key are fixture inputs, not product
/// authority. Uses the ordinary runtime, ledger and Cell formats without a new
/// journal schema. The cell's memory handle retains the backing and manager;
/// bootstrap declarations and policy state are released after opening. No global
/// memory manager, endpoint or lifecycle is installed.
///
/// Fresh memory has an empty cell. Restored callers must reject empty/invalid
/// bytes where their journal contract requires retained state. Callers own
/// serialization, byte/work budgets and same-release recovery checks.
/// [`Self::create_bounded`] and [`Self::open_bounded`] enforce explicit fresh or
/// retained admission plus a caller-selected byte ceiling before payload loading.
///

pub struct FixtureCell<M: Memory> {
    cell: Cell<Vec<u8>, RuntimeMemory<M>>,
    maximum: usize,
}

///
/// FixtureCellError
///
/// Typed admission failure for an explicitly requested fixture byte cell.
/// Runtime sources remain available; payload refusal does not reset cell bytes.
///

#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum FixtureCellError {
    /// Runtime setup failure, preserving its typed source category.
    #[error(transparent)]
    Setup(#[from] FixtureRuntimeError),
    /// The committed key cannot be opened.
    #[error(transparent)]
    Open(#[from] crate::RuntimeOpenError),
    /// Missing, malformed or oversized retained Cell metadata.
    #[error(transparent)]
    Payload(#[from] crate::StableCellPayloadError),
    /// Fresh-cell creation cannot replace an occupied allocation.
    #[error("fixture allocation is already occupied")]
    Occupied,
    /// Candidate bytes exceed the selected write ceiling.
    #[error("fixture value length {size} exceeds byte limit {maximum}")]
    ValueTooLarge { size: usize, maximum: usize },
}

impl<M: Memory> FixtureCell<M> {
    /// Create a bounded empty cell, refusing any previously occupied allocation.
    ///
    /// Bootstrap may update ledger metadata before occupied-cell refusal; existing
    /// cell bytes are never cleared. Callers own payload codecs and initialization.
    ///
    /// # Panics
    /// Backing IO and initial Cell publication can panic.
    pub fn create_bounded(
        memory: M,
        owner: &str,
        namespace: &str,
        stable_key: &str,
        config: MemoryManagerConfig,
        maximum: usize,
    ) -> Result<Self, FixtureCellError> {
        let memory = Self::open_memory(memory, owner, namespace, stable_key, config)?;
        if memory.size() > 0 {
            return Err(FixtureCellError::Occupied);
        }
        Ok(Self::from_admitted_memory(memory, maximum))
    }

    /// Open retained state, checking its header, capacity and byte ceiling first.
    ///
    /// Refuses missing backing without initializing it. The selected Cell must
    /// already exist; its payload is not allocated or read until header admission
    /// succeeds. Bootstrap may advance ledger metadata before later Cell refusal.
    /// Callers still validate payload identity, codec and lifecycle invariants.
    ///
    /// # Panics
    /// Backing IO and Cell loading retain their substrate panic behavior.
    pub fn open_bounded(
        memory: M,
        owner: &str,
        namespace: &str,
        stable_key: &str,
        config: MemoryManagerConfig,
        maximum: usize,
    ) -> Result<Self, FixtureCellError> {
        if memory.size() == 0 {
            return Err(crate::StableCellPayloadError::NotStableCell.into());
        }
        let memory = Self::open_memory(memory, owner, namespace, stable_key, config)?;
        crate::stable_cell::stable_cell_payload_length(&memory, maximum)?;
        Ok(Self::from_admitted_memory(memory, maximum))
    }

    fn open_memory(
        memory: M,
        owner: &str,
        namespace: &str,
        stable_key: &str,
        config: MemoryManagerConfig,
    ) -> Result<RuntimeMemory<M>, FixtureCellError> {
        let runtime = fixture_runtime(memory, owner, namespace, &[stable_key], config)?;
        Ok(runtime.open_memory(stable_key)?)
    }

    fn from_admitted_memory(memory: RuntimeMemory<M>, maximum: usize) -> Self {
        Self {
            cell: Cell::init(memory, Vec::new()),
            maximum,
        }
    }

    /// Transfer the admitted grant for caller-owned raw inspection.
    /// This consumes the fixture without another bootstrap, reset or cleanup.
    #[must_use]
    pub fn into_memory(self) -> RuntimeMemory<M> {
        self.cell.into_memory()
    }

    /// Borrow the original cell bytes without decoding, defaulting or copying them.
    #[must_use]
    pub fn bytes(&self) -> &[u8] {
        self.cell.get()
    }

    /// Persist caller-encoded bytes after checking the selected byte ceiling.
    ///
    /// # Panics
    /// Cell persistence and backing IO can panic after successful length admission.
    pub fn try_set(&mut self, bytes: Vec<u8>) -> Result<(), FixtureCellError> {
        if bytes.len() > self.maximum {
            return Err(FixtureCellError::ValueTooLarge {
                size: bytes.len(),
                maximum: self.maximum,
            });
        }
        self.cell.set(bytes);
        Ok(())
    }
}

#[cfg(test)]
mod tests;