Skip to main content

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}