saorsa_core/upgrade/mod.rs
1// Copyright (c) 2025 Saorsa Labs Limited
2
3// This file is part of the Saorsa P2P network.
4
5// Licensed under the AGPL-3.0 license:
6// <https://www.gnu.org/licenses/agpl-3.0.html>
7
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU Affero General Public License for more details.
12
13// You should have received a copy of the GNU Affero General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16// Copyright 2024 P2P Foundation
17// SPDX-License-Identifier: AGPL-3.0-or-later
18
19//! Auto-upgrade system for cross-platform binary updates.
20//!
21//! This module provides mechanisms for automatic binary updates with:
22//! - Version checking against remote manifest
23//! - Secure download with ML-DSA-65 signature verification
24//! - Platform-specific update application strategies
25//! - Rollback support for failed updates
26//!
27//! # Platform Strategies
28//!
29//! - **Windows**: Rename-and-restart (can't replace running binary)
30//! - **macOS**: Binary replacement with quarantine clearing
31//! - **Linux**: Binary replacement with optional systemd restart
32//!
33//! # Security
34//!
35//! All updates are signed with ML-DSA-65 (post-quantum) signatures.
36//! Signatures must verify before any update is applied.
37//!
38//! # Example
39//!
40//! ```ignore
41//! use saorsa_core::upgrade::{UpdateManager, UpdateConfig, UpdatePolicy};
42//!
43//! let config = UpdateConfig::default();
44//! let manager = DefaultUpdateManager::new(config).await?;
45//!
46//! // Check for updates
47//! if let Some(update) = manager.check_for_updates().await? {
48//! println!("New version available: {}", update.version);
49//!
50//! // Download and verify
51//! let staged = manager.download_update(&update).await?;
52//!
53//! // Apply update (platform-specific)
54//! manager.apply_update(staged).await?;
55//! }
56//! ```
57
58pub mod applier;
59pub mod config;
60pub mod downloader;
61pub mod error;
62pub mod manifest;
63pub mod rollback;
64pub mod staged;
65pub mod verifier;
66
67use async_trait::async_trait;
68
69pub use applier::{ApplierConfig, ApplyResult, UpdateApplier, create_applier};
70pub use config::{PinnedKey, ReleaseChannel, UpdateConfig, UpdateConfigBuilder, UpdatePolicy};
71pub use downloader::{DownloadProgress, Downloader, DownloaderConfig};
72pub use error::UpgradeError;
73pub use manifest::{Platform, PlatformBinary, Release, UpdateManifest};
74pub use rollback::{BackupMetadata, RollbackManager};
75pub use staged::{StagedUpdate, StagedUpdateManager, StagedUpdateMetadata};
76pub use verifier::SignatureVerifier;
77
78use crate::Result;
79
80/// Information about an available update.
81#[derive(Debug, Clone)]
82pub struct UpdateInfo {
83 /// Version string (semver).
84 pub version: String,
85
86 /// Release channel.
87 pub channel: ReleaseChannel,
88
89 /// Whether this is a critical security update.
90 pub is_critical: bool,
91
92 /// Release notes.
93 pub release_notes: String,
94
95 /// Binary information for the current platform.
96 pub binary: PlatformBinary,
97
98 /// URL to the full manifest.
99 pub manifest_url: String,
100}
101
102impl UpdateInfo {
103 /// Check if this update should be applied automatically based on policy.
104 #[must_use]
105 pub fn should_auto_apply(&self, policy: UpdatePolicy) -> bool {
106 match policy {
107 UpdatePolicy::Silent => true,
108 UpdatePolicy::DownloadAndNotify => false,
109 UpdatePolicy::NotifyOnly => false,
110 UpdatePolicy::Manual => false,
111 UpdatePolicy::CriticalOnly => self.is_critical,
112 }
113 }
114}
115
116/// Core trait for update management.
117///
118/// Implementations handle the full update lifecycle:
119/// checking, downloading, verifying, and applying updates.
120#[async_trait]
121pub trait UpdateManager: Send + Sync {
122 /// Check if an update is available.
123 ///
124 /// Fetches the manifest and compares versions.
125 async fn check_for_updates(&self) -> Result<Option<UpdateInfo>>;
126
127 /// Download an update to the staging area.
128 ///
129 /// The downloaded binary is verified before being staged.
130 async fn download_update(&self, update: &UpdateInfo) -> Result<StagedUpdate>;
131
132 /// Apply a staged update.
133 ///
134 /// This uses platform-specific logic:
135 /// - Windows: Rename current binary, move new binary, spawn new process
136 /// - macOS/Linux: Replace binary, optionally restart service
137 async fn apply_update(&self, staged: StagedUpdate) -> Result<()>;
138
139 /// Get current configuration.
140 fn config(&self) -> &UpdateConfig;
141
142 /// Update configuration.
143 fn set_config(&mut self, config: UpdateConfig);
144
145 /// Get the current running version.
146 fn current_version(&self) -> &str;
147
148 /// Rollback to the previous version.
149 ///
150 /// Only works if a backup exists.
151 async fn rollback(&self) -> Result<()>;
152
153 /// Check if a rollback is available.
154 fn can_rollback(&self) -> bool;
155}
156
157/// Event emitted by the upgrade system.
158#[derive(Debug, Clone)]
159pub enum UpgradeEvent {
160 /// Checking for updates.
161 Checking,
162
163 /// Update check completed.
164 CheckComplete {
165 /// Whether an update is available.
166 available: bool,
167 /// Version if available.
168 version: Option<String>,
169 },
170
171 /// Download started.
172 DownloadStarted {
173 /// Version being downloaded.
174 version: String,
175 /// Total size in bytes.
176 total_bytes: u64,
177 },
178
179 /// Download progress.
180 DownloadProgress {
181 /// Bytes downloaded so far.
182 downloaded: u64,
183 /// Total bytes.
184 total: u64,
185 /// Download speed in bytes/second.
186 speed_bps: u64,
187 },
188
189 /// Download completed.
190 DownloadComplete {
191 /// Version downloaded.
192 version: String,
193 },
194
195 /// Verification started.
196 VerificationStarted,
197
198 /// Verification completed.
199 VerificationComplete {
200 /// Whether verification succeeded.
201 success: bool,
202 },
203
204 /// Update being applied.
205 Applying {
206 /// Version being applied.
207 version: String,
208 },
209
210 /// Update applied successfully.
211 Applied {
212 /// New version.
213 version: String,
214 /// Whether restart is required.
215 restart_required: bool,
216 },
217
218 /// Rollback initiated.
219 RollingBack {
220 /// Version rolling back to.
221 to_version: String,
222 },
223
224 /// Rollback completed.
225 RolledBack {
226 /// Version after rollback.
227 version: String,
228 },
229
230 /// Error occurred.
231 Error {
232 /// Error message.
233 message: String,
234 },
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_update_info_should_auto_apply() {
243 let update = UpdateInfo {
244 version: "1.0.0".to_string(),
245 channel: ReleaseChannel::Stable,
246 is_critical: false,
247 release_notes: "Test release".to_string(),
248 binary: PlatformBinary {
249 url: "https://example.com/binary".to_string(),
250 sha256: "abc123".to_string(),
251 signature: "sig123".to_string(),
252 size: 1000,
253 },
254 manifest_url: "https://example.com/manifest".to_string(),
255 };
256
257 assert!(update.should_auto_apply(UpdatePolicy::Silent));
258 assert!(!update.should_auto_apply(UpdatePolicy::Manual));
259 assert!(!update.should_auto_apply(UpdatePolicy::CriticalOnly));
260
261 let critical_update = UpdateInfo {
262 is_critical: true,
263 ..update.clone()
264 };
265 assert!(critical_update.should_auto_apply(UpdatePolicy::CriticalOnly));
266 }
267}