speck-core 0.2.0

Secure runtime package manager for MMU-less microcontrollers
Documentation
//! High-level storage management with transactions and wear leveling
//! 
//! Provides atomic update semantics and manages the layout of:
//! - Boot metadata (version info, rollback protection)
//! - Module slots (active, update, factory)
//! - Journal (transaction log for atomic updates)

use alloc::vec::Vec;
use alloc::string::String;
use crate::error::Result;
use crate::flash::{Flash, PageId};
use crate::format::Module;

pub mod journal;
pub mod layout;
pub mod wear_leveling;

pub use journal::{Journal, Transaction};
pub use layout::StorageLayout;
pub use wear_leveling::WearLevelingPolicy;

/// Storage configuration
#[derive(Clone, Debug)]
pub struct StorageConfig {
    /// Number of module slots (typically 2-3 for A/B+factory)
    pub module_slots: usize,
    /// Enable transaction journaling
    pub enable_journaling: bool,
    /// Enable automatic wear leveling
    pub enable_wear_leveling: bool,
    /// Maximum module size (reserve space per slot)
    pub max_module_size: usize,
}

impl Default for StorageConfig {
    fn default() -> Self {
        Self {
            module_slots: 3, // Active, Update, Factory
            enable_journaling: true,
            enable_wear_leveling: true,
            max_module_size: 32768, // 32KB default
        }
    }
}

/// Installation status
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InstallationStatus {
    /// Slot is empty/erased
    Empty,
    /// Contains valid module
    Valid,
    /// Contains module pending verification
    Pending,
    /// Installation failed/corrupted
    Corrupted,
}

/// High-level storage manager
pub struct StorageManager<'f> {
    flash: &'f mut Flash,
    config: StorageConfig,
    layout: StorageLayout,
    journal: Option<Journal>,
}

impl<'f> StorageManager<'f> {
    /// Create new storage manager
    pub fn new(flash: &'f mut Flash, config: StorageConfig) -> Result<Self> {
        let layout = StorageLayout::new(flash.capacity(), &config)?;
        let journal = if config.enable_journaling {
            Some(Journal::new(layout.journal_region()))
        } else {
            None
        };
        
        Ok(Self {
            flash,
            config,
            layout,
            journal,
        })
    }
    
    /// Read module from slot
    pub fn read_module(&self, slot: usize) -> Result<Option<Module>> {
        if slot >= self.config.module_slots {
            return Err(crate::Error::Storage(format!(
                "invalid slot {}", slot
            )));
        }
        
        let addr = self.layout.slot_address(slot);
        // Read header first to determine size
        let header_data = self.flash.read(addr, crate::format::FIXED_HEADER_SIZE)?;
        
        // Check magic to see if slot is populated
        if &header_data[0..4] != crate::format::MAGIC {
            return Ok(None);
        }
        
        let total_size = u32::from_le_bytes([
            header_data[6], header_data[7], header_data[8], header_data[9]
        ]) as usize;
        
        let full_data = self.flash.read(addr, total_size)?;
        Module::from_bytes(full_data).map(Some)
    }
    
    /// Begin installation to slot (returns transaction)
    pub fn begin_install(&mut self, slot: usize, expected_size: usize) -> Result<Transaction> {
        if slot >= self.config.module_slots {
            return Err(crate::Error::Storage(format!(
                "invalid slot {}", slot
            )));
        }
        
        if expected_size > self.config.max_module_size {
            return Err(crate::Error::CapacityExceeded(format!(
                "module size {} exceeds maximum {}",
                expected_size, self.config.max_module_size
            )));
        }
        
        let addr = self.layout.slot_address(slot);
        let pages_needed = (expected_size + self.flash.page_size() - 1) / self.flash.page_size();
        
        // Erase required pages
        for i in 0..pages_needed {
            let page_addr = addr + i * self.flash.page_size();
            let page_id = PageId(page_addr / self.flash.page_size());
            self.flash.erase_page(page_id)?;
        }
        
        Ok(Transaction::new(slot, addr, expected_size))
    }
    
    /// Commit installation (mark as valid)
    pub fn commit_install(&mut self, tx: Transaction, module: &Module) -> Result<()> {
        // Verify module
        module.verify()?;
        
        // Write to flash
        let data = module.to_bytes()?;
        self.flash.write(tx.address(), &data)?;
        
        // Clear transaction
        if let Some(ref mut journal) = self.journal {
            journal.commit(tx, self.flash)?;
        }
        
        Ok(())
    }
    
    /// Get installation status of slot
    pub fn status(&self, slot: usize) -> Result<InstallationStatus> {
        match self.read_module(slot) {
            Ok(Some(_)) => Ok(InstallationStatus::Valid),
            Ok(None) => Ok(InstallationStatus::Empty),
            Err(_) => Ok(InstallationStatus::Corrupted),
        }
    }
    
    /// Get current monotonic version from metadata
    pub fn current_version(&self) -> Result<u64> {
        let meta_addr = self.layout.metadata_address();
        let data = self.flash.read(meta_addr, 8)?;
        Ok(u64::from_le_bytes([
            data[0], data[1], data[2], data[3],
            data[4], data[5], data[6], data[7],
        ]))
    }
    
    /// Update monotonic version (anti-rollback)
    pub fn update_version(&mut self, version: u64) -> Result<()> {
        let current = self.current_version()?;
        if version <= current {
            return Err(crate::Error::AntiRollback {
                current,
                attempted: version,
            });
        }
        
        let meta_addr = self.layout.metadata_address();
        let data = version.to_le_bytes();
        self.flash.write(meta_addr, &data)?;
        Ok(())
    }
}