use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum UpdateChannel {
Stable,
Beta,
Nightly,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateManifest {
pub version: String,
pub channel: UpdateChannel,
pub url: String,
pub sha256: String,
pub size_bytes: u64,
pub release_notes: Option<String>,
pub min_version: Option<String>,
}
impl UpdateManifest {
pub fn is_compatible_with(&self, current_version: &str) -> bool {
let Some(min) = &self.min_version else {
return true;
};
match (parse_semver(current_version), parse_semver(min)) {
(Some(current), Some(minimum)) => current >= minimum,
_ => false,
}
}
pub fn validate(&self) -> anyhow::Result<()> {
if self.version.is_empty() {
anyhow::bail!("update version must not be empty");
}
if self.url.is_empty() {
anyhow::bail!("update URL must not be empty");
}
if self.sha256.len() != 64 {
anyhow::bail!("sha256 must be exactly 64 hex characters");
}
if !self.sha256.chars().all(|c| c.is_ascii_hexdigit()) {
anyhow::bail!("sha256 must contain only hex characters");
}
if self.size_bytes == 0 {
anyhow::bail!("size_bytes must be greater than zero");
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdatePolicy {
pub channel: UpdateChannel,
pub auto_check: bool,
pub auto_download: bool,
pub auto_install: bool,
pub check_interval_secs: u64,
}
impl UpdatePolicy {
pub fn default_stable() -> Self {
Self {
channel: UpdateChannel::Stable,
auto_check: true,
auto_download: false,
auto_install: false,
check_interval_secs: 86400,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RollbackInfo {
pub previous_version: String,
pub rollback_path: String,
pub created_at: u64,
}
fn parse_semver(version: &str) -> Option<(u64, u64, u64)> {
let parts: Vec<&str> = version.split('.').collect();
if parts.len() != 3 {
return None;
}
Some((
parts[0].parse().ok()?,
parts[1].parse().ok()?,
parts[2].parse().ok()?,
))
}
#[cfg(test)]
mod tests {
use super::*;
fn valid_sha256() -> String {
"a".repeat(64)
}
fn valid_manifest() -> UpdateManifest {
UpdateManifest {
version: "2.0.0".to_string(),
channel: UpdateChannel::Stable,
url: "https://example.com/update.tar.gz".to_string(),
sha256: valid_sha256(),
size_bytes: 1024,
release_notes: Some("Bug fixes".to_string()),
min_version: Some("1.0.0".to_string()),
}
}
#[test]
fn compatible_when_no_min_version() {
let mut m = valid_manifest();
m.min_version = None;
assert!(m.is_compatible_with("0.1.0"));
}
#[test]
fn compatible_when_current_equals_min() {
let m = valid_manifest();
assert!(m.is_compatible_with("1.0.0"));
}
#[test]
fn compatible_when_current_above_min() {
let m = valid_manifest();
assert!(m.is_compatible_with("1.5.0"));
}
#[test]
fn incompatible_when_current_below_min() {
let m = valid_manifest();
assert!(!m.is_compatible_with("0.9.0"));
}
#[test]
fn validate_valid_manifest() {
valid_manifest().validate().unwrap();
}
#[test]
fn validate_empty_version() {
let mut m = valid_manifest();
m.version = String::new();
assert!(m.validate().is_err());
}
#[test]
fn validate_empty_url() {
let mut m = valid_manifest();
m.url = String::new();
assert!(m.validate().is_err());
}
#[test]
fn validate_bad_sha256_length() {
let mut m = valid_manifest();
m.sha256 = "abc".to_string();
assert!(m.validate().is_err());
}
#[test]
fn validate_bad_sha256_chars() {
let mut m = valid_manifest();
m.sha256 = "g".repeat(64);
assert!(m.validate().is_err());
}
#[test]
fn validate_zero_size() {
let mut m = valid_manifest();
m.size_bytes = 0;
assert!(m.validate().is_err());
}
#[test]
fn default_policy_is_conservative() {
let policy = UpdatePolicy::default_stable();
assert!(policy.auto_check);
assert!(!policy.auto_download);
assert!(!policy.auto_install);
assert_eq!(policy.channel, UpdateChannel::Stable);
}
#[test]
fn rollback_info_serialization() {
let info = RollbackInfo {
previous_version: "1.0.0".to_string(),
rollback_path: "/backups/v1.0.0".to_string(),
created_at: 1700000000,
};
let json = serde_json::to_string(&info).unwrap();
let restored: RollbackInfo = serde_json::from_str(&json).unwrap();
assert_eq!(restored.previous_version, "1.0.0");
}
#[test]
fn custom_channel_equality() {
let a = UpdateChannel::Custom("internal".to_string());
let b = UpdateChannel::Custom("internal".to_string());
assert_eq!(a, b);
assert_ne!(a, UpdateChannel::Stable);
}
#[test]
fn manifest_serialization_roundtrip() {
let m = valid_manifest();
let json = serde_json::to_string(&m).unwrap();
let restored: UpdateManifest = serde_json::from_str(&json).unwrap();
assert_eq!(restored.version, "2.0.0");
assert_eq!(restored.channel, UpdateChannel::Stable);
}
}