speck-core 0.2.0

Secure runtime package manager for MMU-less microcontrollers
Documentation
//! Binary delta encoding for efficient updates
//! 
//! Implements a variation of the bsdiff algorithm optimized for embedded systems:
//! - COPY: Copy bytes from source at given offset
//! - INSERT: Insert literal bytes
//! 
//! This produces smaller patches than simple diff for minor updates.

use alloc::vec::Vec;
use crate::error::{Error, Result};

pub mod builder;
pub mod applier;

pub use builder::DeltaBuilder;
pub use applier::DeltaApplier;

/// Magic number for delta files
pub const DELTA_MAGIC: &[u8] = b"SDF\x01";

/// Current delta format version
pub const DELTA_VERSION: u8 = 1;

/// Maximum hunk size to prevent memory exhaustion
pub const MAX_HUNK_SIZE: usize = 65536;

/// Delta patch container
#[derive(Clone, Debug, PartialEq)]
pub struct Delta {
    /// Source size (original file)
    pub source_size: u64,
    /// Target size (result file)
    pub target_size: u64,
    /// Sequence of operations
    pub ops: Vec<Op>,
}

/// Delta operation
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Op {
    /// Copy bytes from source
    Copy {
        /// Offset in source
        src_offset: u64,
        /// Length to copy
        length: u64,
    },
    /// Insert literal bytes
    Insert {
        /// Literal data
        data: Vec<u8>,
    },
}

impl Delta {
    /// Serialize to bytes
    pub fn to_bytes(&self) -> Result<Vec<u8>> {
        let mut result = Vec::new();
        
        // Header
        result.extend_from_slice(DELTA_MAGIC);
        result.push(DELTA_VERSION);
        
        // Sizes
        result.extend_from_slice(&self.source_size.to_le_bytes());
        result.extend_from_slice(&self.target_size.to_le_bytes());
        
        // Operations count
        result.extend_from_slice(&(self.ops.len() as u32).to_le_bytes());
        
        // Operations
        for op in &self.ops {
            match op {
                Op::Copy { src_offset, length } => {
                    result.push(0x01); // COPY opcode
                    result.extend_from_slice(&src_offset.to_le_bytes());
                    result.extend_from_slice(&length.to_le_bytes());
                }
                Op::Insert { data } => {
                    if data.len() > MAX_HUNK_SIZE {
                        return Err(Error::delta(format!(
                            "insert hunk too large: {}", data.len()
                        )));
                    }
                    result.push(0x02); // INSERT opcode
                    result.extend_from_slice(&(data.len() as u32).to_le_bytes());
                    result.extend_from_slice(data);
                }
            }
        }
        
        Ok(result)
    }
    
    /// Deserialize from bytes
    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
        if bytes.len() < 30 {
            return Err(Error::delta("delta too small for header"));
        }
        
        if &bytes[0..4] != DELTA_MAGIC {
            return Err(Error::delta("invalid delta magic"));
        }
        
        if bytes[4] != DELTA_VERSION {
            return Err(Error::delta(format!(
                "unsupported delta version: {}", bytes[4]
            )));
        }
        
        let source_size = u64::from_le_bytes([
            bytes[5], bytes[6], bytes[7], bytes[8],
            bytes[9], bytes[10], bytes[11], bytes[12],
        ]);
        
        let target_size = u64::from_le_bytes([
            bytes[13], bytes[14], bytes[15], bytes[16],
            bytes[17], bytes[18], bytes[19], bytes[20],
        ]);
        
        let op_count = u32::from_le_bytes([bytes[21], bytes[22], bytes[23], bytes[24]]) as usize;
        
        let mut ops = Vec::with_capacity(op_count);
        let mut pos = 25;
        
        for _ in 0..op_count {
            if pos >= bytes.len() {
                return Err(Error::delta("truncated delta ops"));
            }
            
            let opcode = bytes[pos];
            pos += 1;
            
            match opcode {
                0x01 => { // COPY
                    if pos + 16 > bytes.len() {
                        return Err(Error::delta("truncated copy op"));
                    }
                    let src_offset = u64::from_le_bytes([
                        bytes[pos], bytes[pos+1], bytes[pos+2], bytes[pos+3],
                        bytes[pos+4], bytes[pos+5], bytes[pos+6], bytes[pos+7],
                    ]);
                    let length = u64::from_le_bytes([
                        bytes[pos+8], bytes[pos+9], bytes[pos+10], bytes[pos+11],
                        bytes[pos+12], bytes[pos+13], bytes[pos+14], bytes[pos+15],
                    ]);
                    pos += 16;
                    ops.push(Op::Copy { src_offset, length });
                }
                0x02 => { // INSERT
                    if pos + 4 > bytes.len() {
                        return Err(Error::delta("truncated insert header"));
                    }
                    let len = u32::from_le_bytes([bytes[pos], bytes[pos+1], bytes[pos+2], bytes[pos+3]]) as usize;
                    pos += 4;
                    if pos + len > bytes.len() {
                        return Err(Error::delta("truncated insert data"));
                    }
                    if len > MAX_HUNK_SIZE {
                        return Err(Error::delta("insert hunk exceeds maximum"));
                    }
                    ops.push(Op::Insert {
                        data: bytes[pos..pos+len].to_vec(),
                    });
                    pos += len;
                }
                _ => return Err(Error::delta(format!("unknown opcode: {}", opcode))),
            }
        }
        
        Ok(Self {
            source_size,
            target_size,
            ops,
        })
    }
    
    /// Calculate approximate patch size
    pub fn patch_size(&self) -> usize {
        self.ops.iter().map(|op| match op {
            Op::Copy { .. } => 17, // opcode + 2*u64
            Op::Insert { data } => 5 + data.len(), // opcode + u32 + data
        }).sum()
    }
}

/// Simple delta creation for small payloads (fallback)
pub fn create_simple_delta(old: &[u8], new: &[u8]) -> Vec<u8> {
    DeltaBuilder::new()
        .build(old, new)
        .and_then(|d| d.to_bytes())
        .unwrap_or_else(|_| {
            // Fallback: just insert entire new file
            let mut result = DELTA_MAGIC.to_vec();
            result.push(DELTA_VERSION);
            result.extend_from_slice(&(old.len() as u64).to_le_bytes());
            result.extend_from_slice(&(new.len() as u64).to_le_bytes());
            result.extend_from_slice(&1u32.to_le_bytes()); // 1 op
            result.push(0x02); // INSERT
            result.extend_from_slice(&(new.len() as u32).to_le_bytes());
            result.extend_from_slice(new);
            result
        })
}

/// Apply delta to reconstruct target
pub fn apply_delta(source: &[u8], delta: &Delta) -> Result<Vec<u8>> {
    if source.len() as u64 != delta.source_size {
        return Err(Error::delta(format!(
            "source size mismatch: expected {}, got {}",
            delta.source_size, source.len()
        )));
    }
    
    let mut result = Vec::with_capacity(delta.target_size as usize);
    
    for op in &delta.ops {
        match op {
            Op::Copy { src_offset, length } => {
                let start = *src_offset as usize;
                let end = start + *length as usize;
                if end > source.len() {
                    return Err(Error::delta("copy extends past source end"));
                }
                result.extend_from_slice(&source[start..end]);
            }
            Op::Insert { data } => {
                result.extend_from_slice(data);
            }
        }
    }
    
    if result.len() as u64 != delta.target_size {
        return Err(Error::delta(format!(
            "result size mismatch: expected {}, got {}",
            delta.target_size, result.len()
        )));
    }
    
    Ok(result)
}