use crate::error::{Error, Result};
use blake3::Hasher;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateInfo {
pub version: String,
pub url: String,
pub size: u64,
pub hash: String,
pub release_notes: String,
pub critical: bool,
pub min_version: Option<String>,
pub channel: UpdateChannel,
pub released_at: u64,
}
impl UpdateInfo {
pub fn is_compatible(&self, current_version: &str) -> bool {
if let Some(min_ver) = &self.min_version {
match (Version::parse(current_version), Version::parse(min_ver)) {
(Ok(current), Ok(min)) => current >= min,
_ => current_version >= min_ver.as_str(),
}
} else {
true
}
}
pub fn is_newer(&self, current_version: &str) -> bool {
match (
Version::parse(&self.version),
Version::parse(current_version),
) {
(Ok(update_ver), Ok(current)) => update_ver > current,
_ => self.version.as_str() > current_version,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum UpdateChannel {
Stable,
Beta,
Alpha,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum UpdateState {
Idle,
Checking,
Downloading,
Verifying,
Applying,
Completed,
Failed,
RollingBack,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateProgress {
pub state: UpdateState,
pub downloaded: u64,
pub total: u64,
pub percentage: u8,
pub message: String,
}
impl UpdateProgress {
pub fn new(state: UpdateState) -> Self {
Self {
state,
downloaded: 0,
total: 0,
percentage: 0,
message: String::new(),
}
}
pub fn update(&mut self, downloaded: u64, total: u64) {
self.downloaded = downloaded;
self.total = total;
self.percentage = if total > 0 {
((downloaded as f64 / total as f64) * 100.0).min(100.0) as u8
} else {
0
};
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UpdateStats {
pub updates_applied: u64,
pub updates_failed: u64,
pub bytes_downloaded: u64,
pub last_check: u64,
pub last_update: u64,
pub current_version_installed_at: u64,
}
impl UpdateStats {
pub fn new() -> Self {
Self::default()
}
pub fn record_success(&mut self) {
self.updates_applied += 1;
self.last_update = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
self.current_version_installed_at = self.last_update;
}
pub fn record_failure(&mut self) {
self.updates_failed += 1;
}
pub fn success_rate(&self) -> f64 {
let total = self.updates_applied + self.updates_failed;
if total == 0 {
return 0.0;
}
(self.updates_applied as f64) / (total as f64)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OtaConfig {
pub server_url: String,
pub channel: UpdateChannel,
pub auto_download: bool,
pub auto_apply: bool,
pub check_interval_secs: u64,
pub bandwidth_limit: u64,
pub verify_signatures: bool,
}
impl Default for OtaConfig {
fn default() -> Self {
Self {
server_url: String::new(),
channel: UpdateChannel::Stable,
auto_download: false,
auto_apply: false,
check_interval_secs: 86400, bandwidth_limit: 0, verify_signatures: true,
}
}
}
pub struct OtaManager {
current_version: String,
device_id: String,
config: OtaConfig,
progress: UpdateProgress,
stats: UpdateStats,
pending_update: Option<UpdateInfo>,
}
impl OtaManager {
pub fn new(current_version: String, device_id: String) -> Self {
Self {
current_version,
device_id,
config: OtaConfig::default(),
progress: UpdateProgress::new(UpdateState::Idle),
stats: UpdateStats::new(),
pending_update: None,
}
}
pub fn set_update_server(&mut self, url: String) {
self.config.server_url = url;
}
pub fn set_channel(&mut self, channel: UpdateChannel) {
self.config.channel = channel;
}
pub fn set_auto_download(&mut self, enabled: bool) {
self.config.auto_download = enabled;
}
pub fn set_auto_apply(&mut self, enabled: bool) {
self.config.auto_apply = enabled;
}
pub async fn check_for_updates(&mut self) -> Result<Option<UpdateInfo>> {
self.progress.state = UpdateState::Checking;
self.progress.message = "Checking for updates...".to_string();
log::info!("Checking for updates from {}", self.config.server_url);
self.stats.last_check = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
self.progress.state = UpdateState::Idle;
Ok(None)
}
pub async fn download_update(&mut self, update: &UpdateInfo) -> Result<Vec<u8>> {
if !update.is_compatible(&self.current_version) {
return Err(Error::ValidationFailed(format!(
"Update {} is not compatible with current version {}",
update.version, self.current_version
)));
}
self.progress.state = UpdateState::Downloading;
self.progress.update(0, update.size);
self.progress.message = format!("Downloading version {}...", update.version);
log::info!(
"Downloading update {} ({} bytes) from {}",
update.version,
update.size,
update.url
);
let firmware = vec![0u8; update.size as usize];
self.progress.update(update.size, update.size);
self.stats.bytes_downloaded += update.size;
self.progress.state = UpdateState::Verifying;
self.progress.message = "Verifying download...".to_string();
self.verify_firmware(&firmware, &update.hash)?;
self.pending_update = Some(update.clone());
self.progress.state = UpdateState::Idle;
Ok(firmware)
}
fn verify_firmware(&self, firmware: &[u8], expected_hash: &str) -> Result<()> {
let mut hasher = Hasher::new();
hasher.update(firmware);
let hash = hex::encode(hasher.finalize().as_bytes());
if hash.to_lowercase() != expected_hash.to_lowercase() {
return Err(Error::ValidationFailed(format!(
"Firmware hash mismatch: expected {}, got {}",
expected_hash, hash
)));
}
log::info!("Firmware hash verified: {}", hash);
Ok(())
}
pub fn apply_update(&mut self, firmware: &[u8]) -> Result<()> {
if self.pending_update.is_none() {
return Err(Error::ValidationFailed(
"No pending update to apply".to_string(),
));
}
let update = self.pending_update.as_ref().unwrap();
self.progress.state = UpdateState::Applying;
self.progress.message = format!("Applying update {}...", update.version);
log::info!(
"Applying update {} ({} bytes)",
update.version,
firmware.len()
);
self.current_version = update.version.clone();
self.progress.state = UpdateState::Completed;
self.progress.message = "Update applied successfully".to_string();
self.stats.record_success();
self.pending_update = None;
log::info!(
"Update applied successfully, now running version {}",
self.current_version
);
Ok(())
}
pub fn rollback(&mut self) -> Result<()> {
self.progress.state = UpdateState::RollingBack;
self.progress.message = "Rolling back to previous version...".to_string();
log::warn!("Rolling back update");
self.progress.state = UpdateState::Failed;
self.stats.record_failure();
Err(Error::Internal("Rollback not implemented".to_string()))
}
pub fn current_version(&self) -> &str {
&self.current_version
}
pub fn progress(&self) -> &UpdateProgress {
&self.progress
}
pub fn stats(&self) -> &UpdateStats {
&self.stats
}
pub fn pending_update(&self) -> Option<&UpdateInfo> {
self.pending_update.as_ref()
}
pub fn cancel_update(&mut self) {
self.pending_update = None;
self.progress = UpdateProgress::new(UpdateState::Idle);
log::info!("Update cancelled");
}
pub fn is_updating(&self) -> bool {
matches!(
self.progress.state,
UpdateState::Downloading | UpdateState::Verifying | UpdateState::Applying
)
}
pub fn device_id(&self) -> &str {
&self.device_id
}
pub fn config(&self) -> &OtaConfig {
&self.config
}
pub fn set_config(&mut self, config: OtaConfig) {
self.config = config;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Partition {
A,
B,
}
impl Partition {
pub fn other(&self) -> Partition {
match self {
Partition::A => Partition::B,
Partition::B => Partition::A,
}
}
}
pub mod utils {
use super::*;
use std::time::Duration;
pub fn compare_versions(v1: &str, v2: &str) -> std::cmp::Ordering {
match (Version::parse(v1), Version::parse(v2)) {
(Ok(ver1), Ok(ver2)) => ver1.cmp(&ver2),
_ => v1.cmp(v2),
}
}
pub fn is_valid_version(version: &str) -> bool {
!version.is_empty()
&& version
.chars()
.all(|c| c.is_alphanumeric() || c == '.' || c == '-')
}
pub fn calculate_hash(firmware: &[u8]) -> String {
let mut hasher = Hasher::new();
hasher.update(firmware);
hex::encode(hasher.finalize().as_bytes())
}
pub fn estimate_download_time(size_bytes: u64, bandwidth_bps: u64) -> Duration {
if bandwidth_bps == 0 {
return Duration::from_secs(0);
}
let seconds = (size_bytes * 8) / bandwidth_bps;
Duration::from_secs(seconds)
}
}
#[allow(dead_code)]
mod hex {
pub fn encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
pub fn decode(s: &str) -> Result<Vec<u8>, String> {
if s.len() % 2 != 0 {
return Err("Invalid hex string length".to_string());
}
(0..s.len())
.step_by(2)
.map(|i| {
u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| format!("Invalid hex: {}", e))
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_update_info_compatibility() {
let update = UpdateInfo {
version: "2.0.0".to_string(),
url: "https://example.com/firmware.bin".to_string(),
size: 1024,
hash: "abcd1234".to_string(),
release_notes: "New features".to_string(),
critical: false,
min_version: Some("1.5.0".to_string()),
channel: UpdateChannel::Stable,
released_at: 0,
};
assert!(update.is_compatible("1.5.0"));
assert!(update.is_compatible("1.9.0"));
assert!(!update.is_compatible("1.4.0"));
}
#[test]
fn test_update_info_newer() {
let update = UpdateInfo {
version: "2.0.0".to_string(),
url: "https://example.com/firmware.bin".to_string(),
size: 1024,
hash: "abcd1234".to_string(),
release_notes: "New features".to_string(),
critical: false,
min_version: None,
channel: UpdateChannel::Stable,
released_at: 0,
};
assert!(update.is_newer("1.9.0"));
assert!(!update.is_newer("2.0.0"));
assert!(!update.is_newer("2.1.0"));
}
#[test]
fn test_update_progress() {
let mut progress = UpdateProgress::new(UpdateState::Downloading);
progress.update(512, 1024);
assert_eq!(progress.downloaded, 512);
assert_eq!(progress.total, 1024);
assert_eq!(progress.percentage, 50);
}
#[test]
fn test_update_stats() {
let mut stats = UpdateStats::new();
stats.record_success();
stats.record_failure();
assert_eq!(stats.updates_applied, 1);
assert_eq!(stats.updates_failed, 1);
assert_eq!(stats.success_rate(), 0.5);
}
#[test]
fn test_ota_manager_creation() {
let ota = OtaManager::new("1.0.0".to_string(), "device-123".to_string());
assert_eq!(ota.current_version(), "1.0.0");
assert_eq!(ota.device_id(), "device-123");
assert!(!ota.is_updating());
}
#[test]
fn test_ota_config() {
let mut ota = OtaManager::new("1.0.0".to_string(), "device-123".to_string());
ota.set_update_server("https://updates.example.com".to_string());
ota.set_channel(UpdateChannel::Beta);
ota.set_auto_download(true);
assert_eq!(ota.config().server_url, "https://updates.example.com");
assert_eq!(ota.config().channel, UpdateChannel::Beta);
assert!(ota.config().auto_download);
}
#[test]
fn test_partition() {
assert_eq!(Partition::A.other(), Partition::B);
assert_eq!(Partition::B.other(), Partition::A);
}
#[test]
fn test_version_validation() {
assert!(utils::is_valid_version("1.0.0"));
assert!(utils::is_valid_version("2.0.0-beta.1"));
assert!(!utils::is_valid_version(""));
assert!(!utils::is_valid_version("1.0.0 invalid"));
}
#[test]
fn test_hash_calculation() {
let data = b"test firmware data";
let hash = utils::calculate_hash(data);
assert_eq!(hash.len(), 64); }
#[test]
fn test_download_time_estimation() {
let size = 1_000_000; let bandwidth = 1_000_000; let time = utils::estimate_download_time(size, bandwidth);
assert_eq!(time, Duration::from_secs(8));
}
#[test]
fn test_hex_encoding() {
let data = vec![0xab, 0xcd, 0xef, 0x12];
let encoded = hex::encode(&data);
assert_eq!(encoded, "abcdef12");
let decoded = hex::decode(&encoded).unwrap();
assert_eq!(decoded, data);
}
#[test]
fn test_update_info_clone_debug() {
let update = UpdateInfo {
version: "2.0.0".to_string(),
url: "https://example.com".to_string(),
size: 1024,
hash: "abc123".to_string(),
release_notes: "Notes".to_string(),
critical: true,
min_version: None,
channel: UpdateChannel::Stable,
released_at: 12345,
};
let cloned = update.clone();
assert_eq!(cloned.version, "2.0.0");
assert_eq!(cloned.critical, true);
let debug = format!("{:?}", update);
assert!(debug.contains("UpdateInfo"));
assert!(debug.contains("2.0.0"));
}
#[test]
fn test_update_info_serialize() {
let update = UpdateInfo {
version: "2.0.0".to_string(),
url: "https://example.com".to_string(),
size: 1024,
hash: "abc123".to_string(),
release_notes: "Notes".to_string(),
critical: false,
min_version: Some("1.0.0".to_string()),
channel: UpdateChannel::Beta,
released_at: 12345,
};
let json = serde_json::to_string(&update).unwrap();
let parsed: UpdateInfo = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.version, "2.0.0");
assert_eq!(parsed.channel, UpdateChannel::Beta);
}
#[test]
fn test_update_info_is_compatible_no_min_version() {
let update = UpdateInfo {
version: "2.0.0".to_string(),
url: "".to_string(),
size: 0,
hash: "".to_string(),
release_notes: "".to_string(),
critical: false,
min_version: None,
channel: UpdateChannel::Stable,
released_at: 0,
};
assert!(update.is_compatible("0.0.1"));
assert!(update.is_compatible("9.9.9"));
}
#[test]
fn test_update_channel_all_variants() {
let channels = [
UpdateChannel::Stable,
UpdateChannel::Beta,
UpdateChannel::Alpha,
];
for channel in channels {
let cloned = channel;
assert_eq!(channel, cloned);
let debug = format!("{:?}", channel);
assert!(!debug.is_empty());
}
}
#[test]
fn test_update_channel_serialize() {
for channel in [
UpdateChannel::Stable,
UpdateChannel::Beta,
UpdateChannel::Alpha,
] {
let json = serde_json::to_string(&channel).unwrap();
let parsed: UpdateChannel = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, channel);
}
}
#[test]
fn test_update_state_all_variants() {
let states = [
UpdateState::Idle,
UpdateState::Checking,
UpdateState::Downloading,
UpdateState::Verifying,
UpdateState::Applying,
UpdateState::Completed,
UpdateState::Failed,
UpdateState::RollingBack,
];
for state in states {
let cloned = state;
assert_eq!(state, cloned);
let debug = format!("{:?}", state);
assert!(!debug.is_empty());
}
}
#[test]
fn test_update_state_serialize() {
for state in [
UpdateState::Idle,
UpdateState::Downloading,
UpdateState::Failed,
] {
let json = serde_json::to_string(&state).unwrap();
let parsed: UpdateState = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, state);
}
}
#[test]
fn test_update_progress_zero_total() {
let mut progress = UpdateProgress::new(UpdateState::Downloading);
progress.update(0, 0);
assert_eq!(progress.percentage, 0);
}
#[test]
fn test_update_progress_serialize() {
let progress = UpdateProgress::new(UpdateState::Verifying);
let json = serde_json::to_string(&progress).unwrap();
let parsed: UpdateProgress = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.state, UpdateState::Verifying);
}
#[test]
fn test_update_progress_debug_clone() {
let progress = UpdateProgress::new(UpdateState::Applying);
let cloned = progress.clone();
assert_eq!(cloned.state, UpdateState::Applying);
let debug = format!("{:?}", progress);
assert!(debug.contains("UpdateProgress"));
}
#[test]
fn test_update_stats_success_rate_empty() {
let stats = UpdateStats::new();
assert_eq!(stats.success_rate(), 0.0);
}
#[test]
fn test_update_stats_default() {
let stats = UpdateStats::default();
assert_eq!(stats.updates_applied, 0);
assert_eq!(stats.updates_failed, 0);
assert_eq!(stats.bytes_downloaded, 0);
}
#[test]
fn test_update_stats_serialize() {
let mut stats = UpdateStats::new();
stats.record_success();
let json = serde_json::to_string(&stats).unwrap();
let parsed: UpdateStats = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.updates_applied, 1);
}
#[test]
fn test_ota_config_default() {
let config = OtaConfig::default();
assert!(config.server_url.is_empty());
assert_eq!(config.channel, UpdateChannel::Stable);
assert!(!config.auto_download);
assert!(!config.auto_apply);
assert_eq!(config.check_interval_secs, 86400);
assert_eq!(config.bandwidth_limit, 0);
assert!(config.verify_signatures);
}
#[test]
fn test_ota_config_clone_debug() {
let config = OtaConfig::default();
let cloned = config.clone();
assert_eq!(cloned.channel, UpdateChannel::Stable);
let debug = format!("{:?}", config);
assert!(debug.contains("OtaConfig"));
}
#[test]
fn test_ota_config_serialize() {
let config = OtaConfig {
server_url: "https://test.com".to_string(),
channel: UpdateChannel::Alpha,
auto_download: true,
auto_apply: true,
check_interval_secs: 3600,
bandwidth_limit: 1000,
verify_signatures: false,
};
let json = serde_json::to_string(&config).unwrap();
let parsed: OtaConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.channel, UpdateChannel::Alpha);
assert!(parsed.auto_download);
}
#[test]
fn test_ota_manager_set_auto_apply() {
let mut ota = OtaManager::new("1.0.0".to_string(), "device".to_string());
assert!(!ota.config().auto_apply);
ota.set_auto_apply(true);
assert!(ota.config().auto_apply);
}
#[test]
fn test_ota_manager_set_config() {
let mut ota = OtaManager::new("1.0.0".to_string(), "device".to_string());
let config = OtaConfig {
server_url: "https://new.com".to_string(),
channel: UpdateChannel::Beta,
auto_download: true,
auto_apply: true,
check_interval_secs: 1800,
bandwidth_limit: 5000,
verify_signatures: false,
};
ota.set_config(config);
assert_eq!(ota.config().server_url, "https://new.com");
assert_eq!(ota.config().channel, UpdateChannel::Beta);
}
#[test]
fn test_ota_manager_cancel_update() {
let mut ota = OtaManager::new("1.0.0".to_string(), "device".to_string());
ota.cancel_update();
assert!(ota.pending_update().is_none());
assert_eq!(ota.progress().state, UpdateState::Idle);
}
#[test]
fn test_ota_manager_is_updating() {
let ota = OtaManager::new("1.0.0".to_string(), "device".to_string());
assert!(!ota.is_updating());
}
#[test]
fn test_ota_manager_progress() {
let ota = OtaManager::new("1.0.0".to_string(), "device".to_string());
let progress = ota.progress();
assert_eq!(progress.state, UpdateState::Idle);
}
#[test]
fn test_ota_manager_stats() {
let ota = OtaManager::new("1.0.0".to_string(), "device".to_string());
let stats = ota.stats();
assert_eq!(stats.updates_applied, 0);
}
#[test]
fn test_partition_debug() {
let partition = Partition::A;
let debug = format!("{:?}", partition);
assert!(debug.contains("A"));
}
#[test]
fn test_partition_clone_eq() {
let p1 = Partition::A;
let p2 = Partition::A;
let p3 = Partition::B;
assert_eq!(p1, p2);
assert_ne!(p1, p3);
}
#[test]
fn test_hex_decode_invalid_length() {
let result = hex::decode("abc"); assert!(result.is_err());
}
#[test]
fn test_hex_decode_invalid_chars() {
let result = hex::decode("ghij"); assert!(result.is_err());
}
#[test]
fn test_hex_encode_empty() {
let encoded = hex::encode(&[]);
assert!(encoded.is_empty());
}
#[test]
fn test_version_compare() {
use std::cmp::Ordering;
assert_eq!(utils::compare_versions("1.0.0", "2.0.0"), Ordering::Less);
assert_eq!(utils::compare_versions("2.0.0", "1.0.0"), Ordering::Greater);
assert_eq!(utils::compare_versions("1.0.0", "1.0.0"), Ordering::Equal);
}
#[test]
fn test_download_time_zero_bandwidth() {
let time = utils::estimate_download_time(1000, 0);
assert_eq!(time, Duration::from_secs(0));
}
}