133 lines
5.2 KiB
Rust
133 lines
5.2 KiB
Rust
#![deny(warnings)]
|
|
//! [TSM.ID].[11031972] -- Platform X Ecosystem
|
|
//! xcu-ouroboros -- Self-updating Binary Manager with OTA & Integrity
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Debug)]
|
|
pub enum OuroborosError { VersionConflict(String), IntegrityFailed(String), RollbackFailed(String) }
|
|
impl std::fmt::Display for OuroborosError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self { Self::VersionConflict(e) => write!(f, "Version: {e}"), Self::IntegrityFailed(e) => write!(f, "Integrity: {e}"), Self::RollbackFailed(e) => write!(f, "Rollback: {e}") }
|
|
}
|
|
}
|
|
impl std::error::Error for OuroborosError {}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct BinaryVersion { pub version: String, pub hash: [u8; 32], pub size_bytes: u64, pub timestamp: u64, pub changelog: String }
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum UpdateState { Idle, Downloading, Verifying, Swapping, Rollback, Complete, Failed }
|
|
|
|
pub struct Ouroboros {
|
|
current: BinaryVersion,
|
|
history: Vec<BinaryVersion>,
|
|
state: UpdateState,
|
|
max_rollback: usize,
|
|
}
|
|
|
|
impl Ouroboros {
|
|
pub fn new(current: BinaryVersion, max_rollback: usize) -> Self {
|
|
Self { current, history: Vec::new(), state: UpdateState::Idle, max_rollback }
|
|
}
|
|
|
|
/// Verify binary integrity using FNV hash
|
|
pub fn verify_integrity(&self, binary_data: &[u8], expected_hash: &[u8; 32]) -> Result<bool, OuroborosError> {
|
|
let hash = Self::compute_hash(binary_data);
|
|
if hash != *expected_hash {
|
|
return Err(OuroborosError::IntegrityFailed(
|
|
format!("Hash mismatch: computed {:02x}{:02x}..., expected {:02x}{:02x}...", hash[0], hash[1], expected_hash[0], expected_hash[1])));
|
|
}
|
|
Ok(true)
|
|
}
|
|
|
|
fn compute_hash(data: &[u8]) -> [u8; 32] {
|
|
let mut hash = [0u8; 32];
|
|
let mut state: u64 = 0xcbf29ce484222325;
|
|
for (i, &b) in data.iter().enumerate() {
|
|
state ^= b as u64;
|
|
state = state.wrapping_mul(0x100000001b3);
|
|
if i % 4 == 0 { hash[i % 32] ^= (state & 0xFF) as u8; }
|
|
}
|
|
for i in 0..32 { hash[i] ^= ((state >> (i % 8 * 8)) & 0xFF) as u8; }
|
|
hash
|
|
}
|
|
|
|
/// Stage update: download → verify → swap
|
|
pub fn stage_update(&mut self, new_version: BinaryVersion, binary_data: &[u8]) -> Result<(), OuroborosError> {
|
|
self.state = UpdateState::Downloading;
|
|
// Verify
|
|
self.state = UpdateState::Verifying;
|
|
self.verify_integrity(binary_data, &new_version.hash)?;
|
|
// Compare versions
|
|
if new_version.version == self.current.version {
|
|
return Err(OuroborosError::VersionConflict(format!("Already at {}", self.current.version)));
|
|
}
|
|
// Swap
|
|
self.state = UpdateState::Swapping;
|
|
self.history.push(self.current.clone());
|
|
if self.history.len() > self.max_rollback { self.history.remove(0); }
|
|
self.current = new_version;
|
|
self.state = UpdateState::Complete;
|
|
Ok(())
|
|
}
|
|
|
|
/// Rollback to previous version
|
|
pub fn rollback(&mut self) -> Result<BinaryVersion, OuroborosError> {
|
|
self.state = UpdateState::Rollback;
|
|
let prev = self.history.pop().ok_or_else(|| OuroborosError::RollbackFailed("No previous version".into()))?;
|
|
self.current = prev.clone();
|
|
self.state = UpdateState::Complete;
|
|
Ok(prev)
|
|
}
|
|
|
|
pub fn current_version(&self) -> &BinaryVersion { &self.current }
|
|
pub fn state(&self) -> &UpdateState { &self.state }
|
|
pub fn rollback_depth(&self) -> usize { self.history.len() }
|
|
|
|
/// Version comparison (semver-like)
|
|
pub fn is_newer(current: &str, candidate: &str) -> bool {
|
|
let parse = |v: &str| -> Vec<u32> { v.split('.').filter_map(|s| s.parse().ok()).collect() };
|
|
let c = parse(current);
|
|
let n = parse(candidate);
|
|
for i in 0..c.len().max(n.len()) {
|
|
let cv = c.get(i).copied().unwrap_or(0);
|
|
let nv = n.get(i).copied().unwrap_or(0);
|
|
if nv > cv { return true; }
|
|
if nv < cv { return false; }
|
|
}
|
|
false
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
fn v1() -> BinaryVersion { BinaryVersion { version: "1.0.0".into(), hash: [0u8; 32], size_bytes: 1000, timestamp: 100, changelog: "init".into() } }
|
|
#[test]
|
|
fn test_version_compare() {
|
|
assert!(Ouroboros::is_newer("1.0.0", "1.0.1"));
|
|
assert!(Ouroboros::is_newer("1.0.0", "2.0.0"));
|
|
assert!(!Ouroboros::is_newer("2.0.0", "1.0.0"));
|
|
}
|
|
#[test]
|
|
fn test_integrity() {
|
|
let o = Ouroboros::new(v1(), 3);
|
|
let data = b"test binary";
|
|
let hash = Ouroboros::compute_hash(data);
|
|
assert!(o.verify_integrity(data, &hash).is_ok());
|
|
let bad_hash = [0xFF; 32];
|
|
assert!(o.verify_integrity(data, &bad_hash).is_err());
|
|
}
|
|
#[test]
|
|
fn test_rollback() {
|
|
let data = b"new binary";
|
|
let hash = Ouroboros::compute_hash(data);
|
|
let mut o = Ouroboros::new(v1(), 3);
|
|
let v2 = BinaryVersion { version: "2.0.0".into(), hash, size_bytes: 500, timestamp: 200, changelog: "v2".into() };
|
|
o.stage_update(v2, data).unwrap();
|
|
assert_eq!(o.current_version().version, "2.0.0");
|
|
let prev = o.rollback().unwrap();
|
|
assert_eq!(prev.version, "1.0.0");
|
|
}
|
|
}
|