1mod apply;
11mod binary_cache;
12mod cache_dir;
13mod monitor;
14mod release_cache;
15mod rollout;
16mod signature;
17
18pub use apply::{AutoApplyUpgrader, RESTART_EXIT_CODE};
19pub use binary_cache::BinaryCache;
20pub use cache_dir::upgrade_cache_dir;
21pub use monitor::{find_platform_asset, version_from_tag, Asset, GitHubRelease, UpgradeMonitor};
22pub use release_cache::ReleaseCache;
23pub use rollout::StagedRollout;
24pub use signature::{
25 verify_binary_signature, verify_binary_signature_with_key, verify_from_file,
26 verify_from_file_with_key, PUBLIC_KEY_SIZE, SIGNATURE_SIZE, SIGNING_CONTEXT,
27};
28
29use crate::error::{Error, Result};
30use semver::Version;
31use std::fs;
32use std::path::Path;
33use tracing::{debug, info, warn};
34
35const MAX_BINARY_SIZE_BYTES: usize = 200 * 1024 * 1024;
40
41#[derive(Debug, Clone)]
43pub struct UpgradeInfo {
44 pub version: Version,
46 pub download_url: String,
48 pub signature_url: String,
50 pub release_notes: String,
52}
53
54#[derive(Debug)]
56pub enum UpgradeResult {
57 Success {
59 version: Version,
61 exit_code: i32,
64 },
65 RolledBack {
67 reason: String,
69 },
70 NoUpgrade,
72}
73
74pub struct Upgrader {
84 current_version: Version,
86 client: reqwest::Client,
88}
89
90impl Upgrader {
91 #[must_use]
93 pub fn new() -> Self {
94 let current_version =
95 Version::parse(env!("CARGO_PKG_VERSION")).unwrap_or_else(|_| Version::new(0, 0, 0));
96
97 Self {
98 current_version,
99 client: reqwest::Client::new(),
100 }
101 }
102
103 #[cfg(test)]
105 #[must_use]
106 pub fn with_version(version: Version) -> Self {
107 Self {
108 current_version: version,
109 client: reqwest::Client::new(),
110 }
111 }
112
113 #[must_use]
115 pub fn current_version(&self) -> &Version {
116 &self.current_version
117 }
118
119 pub fn validate_upgrade(&self, info: &UpgradeInfo) -> Result<()> {
125 if info.version <= self.current_version {
126 return Err(Error::Upgrade(format!(
127 "Cannot downgrade from {} to {}",
128 self.current_version, info.version
129 )));
130 }
131 Ok(())
132 }
133
134 pub fn create_backup(&self, current: &Path, rollback_dir: &Path) -> Result<()> {
145 let filename = current
146 .file_name()
147 .ok_or_else(|| Error::Upgrade("Invalid binary path".to_string()))?;
148
149 let backup_path = rollback_dir.join(format!("{}.backup", filename.to_string_lossy()));
150
151 debug!("Creating backup at: {}", backup_path.display());
152 fs::copy(current, &backup_path)?;
153 Ok(())
154 }
155
156 pub fn restore_from_backup(&self, current: &Path, rollback_dir: &Path) -> Result<()> {
167 let filename = current
168 .file_name()
169 .ok_or_else(|| Error::Upgrade("Invalid binary path".to_string()))?;
170
171 let backup_path = rollback_dir.join(format!("{}.backup", filename.to_string_lossy()));
172
173 if !backup_path.exists() {
174 return Err(Error::Upgrade("No backup found for rollback".to_string()));
175 }
176
177 info!("Restoring from backup: {}", backup_path.display());
178 fs::copy(&backup_path, current)?;
179 Ok(())
180 }
181
182 pub fn atomic_replace(&self, new_binary: &Path, target: &Path) -> Result<()> {
195 #[cfg(unix)]
197 {
198 if let Ok(meta) = fs::metadata(target) {
199 let perms = meta.permissions();
200 fs::set_permissions(new_binary, perms)?;
201 }
202 }
203
204 fs::rename(new_binary, target)?;
206 debug!("Atomic replacement complete");
207 Ok(())
208 }
209
210 async fn download(&self, url: &str, dest: &Path) -> Result<()> {
216 debug!("Downloading: {}", url);
217
218 let response = self
219 .client
220 .get(url)
221 .send()
222 .await
223 .map_err(|e| Error::Network(format!("Download failed: {e}")))?;
224
225 if !response.status().is_success() {
226 return Err(Error::Network(format!(
227 "Download returned status: {}",
228 response.status()
229 )));
230 }
231
232 let bytes = response
233 .bytes()
234 .await
235 .map_err(|e| Error::Network(format!("Failed to read response: {e}")))?;
236
237 Self::enforce_max_binary_size(bytes.len())?;
238
239 fs::write(dest, &bytes)?;
240 debug!("Downloaded {} bytes to {}", bytes.len(), dest.display());
241 Ok(())
242 }
243
244 fn enforce_max_binary_size(len: usize) -> Result<()> {
246 if len > MAX_BINARY_SIZE_BYTES {
247 return Err(Error::Upgrade(format!(
248 "Downloaded binary too large: {len} bytes (max {MAX_BINARY_SIZE_BYTES})"
249 )));
250 }
251 Ok(())
252 }
253
254 fn create_tempdir_in_target_dir(current_binary: &Path) -> Result<tempfile::TempDir> {
258 let target_dir = current_binary
259 .parent()
260 .ok_or_else(|| Error::Upgrade("Current binary has no parent directory".to_string()))?;
261
262 tempfile::Builder::new()
263 .prefix("ant-upgrade-")
264 .tempdir_in(target_dir)
265 .map_err(|e| Error::Upgrade(format!("Failed to create temp dir: {e}")))
266 }
267
268 pub async fn perform_upgrade(
288 &self,
289 info: &UpgradeInfo,
290 current_binary: &Path,
291 rollback_dir: &Path,
292 ) -> Result<UpgradeResult> {
293 if !Self::auto_upgrade_supported() {
296 warn!(
297 "Auto-upgrade is not supported on this platform; refusing upgrade to {}",
298 info.version
299 );
300 return Ok(UpgradeResult::RolledBack {
301 reason: "Auto-upgrade not supported on this platform".to_string(),
302 });
303 }
304
305 self.validate_upgrade(info)?;
307
308 self.create_backup(current_binary, rollback_dir)?;
310
311 let temp_dir = Self::create_tempdir_in_target_dir(current_binary)?;
313 let new_binary = temp_dir.path().join("new_binary");
314 let sig_path = temp_dir.path().join("signature");
315
316 if let Err(e) = self.download(&info.download_url, &new_binary).await {
317 warn!("Download failed: {e}");
318 return Ok(UpgradeResult::RolledBack {
319 reason: format!("Download failed: {e}"),
320 });
321 }
322
323 if let Err(e) = self.download(&info.signature_url, &sig_path).await {
324 warn!("Signature download failed: {e}");
325 return Ok(UpgradeResult::RolledBack {
326 reason: format!("Signature download failed: {e}"),
327 });
328 }
329
330 if let Err(e) = signature::verify_from_file(&new_binary, &sig_path) {
332 warn!("Signature verification failed: {e}");
333 return Ok(UpgradeResult::RolledBack {
334 reason: format!("Signature verification failed: {e}"),
335 });
336 }
337
338 if let Err(e) = self.atomic_replace(&new_binary, current_binary) {
340 warn!("Replacement failed, rolling back: {e}");
341 if let Err(restore_err) = self.restore_from_backup(current_binary, rollback_dir) {
342 return Err(Error::Upgrade(format!(
343 "Critical: replacement failed ({e}) AND rollback failed ({restore_err})"
344 )));
345 }
346 return Ok(UpgradeResult::RolledBack {
347 reason: format!("Replacement failed: {e}"),
348 });
349 }
350
351 info!("Successfully upgraded to version {}", info.version);
352 Ok(UpgradeResult::Success {
353 version: info.version.clone(),
354 exit_code: 0,
355 })
356 }
357
358 const fn auto_upgrade_supported() -> bool {
362 true
363 }
364}
365
366impl Default for Upgrader {
367 fn default() -> Self {
368 Self::new()
369 }
370}
371
372pub async fn perform_upgrade(
378 info: &UpgradeInfo,
379 current_binary: &Path,
380 rollback_dir: &Path,
381) -> Result<UpgradeResult> {
382 Upgrader::new()
383 .perform_upgrade(info, current_binary, rollback_dir)
384 .await
385}
386
387#[cfg(test)]
388#[allow(
389 clippy::unwrap_used,
390 clippy::expect_used,
391 clippy::doc_markdown,
392 clippy::cast_possible_truncation,
393 clippy::cast_sign_loss,
394 clippy::case_sensitive_file_extension_comparisons
395)]
396mod tests {
397 use super::*;
398 use tempfile::TempDir;
399
400 #[test]
402 fn test_backup_created() {
403 let temp = TempDir::new().unwrap();
404 let current = temp.path().join("current");
405 let rollback_dir = temp.path().join("rollback");
406 fs::create_dir(&rollback_dir).unwrap();
407
408 let original_content = b"old binary content";
409 fs::write(¤t, original_content).unwrap();
410
411 let upgrader = Upgrader::new();
412 upgrader.create_backup(¤t, &rollback_dir).unwrap();
413
414 let backup_path = rollback_dir.join("current.backup");
415 assert!(backup_path.exists(), "Backup file should exist");
416 assert_eq!(
417 fs::read(&backup_path).unwrap(),
418 original_content,
419 "Backup content should match"
420 );
421 }
422
423 #[test]
425 fn test_restore_from_backup() {
426 let temp = TempDir::new().unwrap();
427 let current = temp.path().join("binary");
428 let rollback_dir = temp.path().join("rollback");
429 fs::create_dir(&rollback_dir).unwrap();
430
431 let original = b"original content";
432 fs::write(¤t, original).unwrap();
433
434 let upgrader = Upgrader::new();
435 upgrader.create_backup(¤t, &rollback_dir).unwrap();
436
437 fs::write(¤t, b"corrupted content").unwrap();
439
440 upgrader
442 .restore_from_backup(¤t, &rollback_dir)
443 .unwrap();
444
445 assert_eq!(fs::read(¤t).unwrap(), original);
446 }
447
448 #[test]
450 fn test_atomic_replacement() {
451 let temp = TempDir::new().unwrap();
452 let current = temp.path().join("binary");
453 let new_binary = temp.path().join("new_binary");
454
455 fs::write(¤t, b"old").unwrap();
456 fs::write(&new_binary, b"new").unwrap();
457
458 let upgrader = Upgrader::new();
459 upgrader.atomic_replace(&new_binary, ¤t).unwrap();
460
461 assert_eq!(fs::read(¤t).unwrap(), b"new");
462 assert!(!new_binary.exists(), "Source should be moved, not copied");
463 }
464
465 #[test]
467 fn test_downgrade_prevention() {
468 let current_version = Version::new(1, 1, 0);
469 let older_version = Version::new(1, 0, 0);
470
471 let upgrader = Upgrader::with_version(current_version);
472
473 let info = UpgradeInfo {
474 version: older_version,
475 download_url: "test".to_string(),
476 signature_url: "test.sig".to_string(),
477 release_notes: "Old".to_string(),
478 };
479
480 let result = upgrader.validate_upgrade(&info);
481 assert!(result.is_err());
482 let err_msg = result.unwrap_err().to_string();
483 assert!(
484 err_msg.contains("downgrade") || err_msg.contains("Cannot"),
485 "Error should mention downgrade prevention: {err_msg}"
486 );
487 }
488
489 #[test]
491 fn test_same_version_prevention() {
492 let version = Version::new(1, 0, 0);
493 let upgrader = Upgrader::with_version(version.clone());
494
495 let info = UpgradeInfo {
496 version,
497 download_url: "test".to_string(),
498 signature_url: "test.sig".to_string(),
499 release_notes: "Same".to_string(),
500 };
501
502 let result = upgrader.validate_upgrade(&info);
503 assert!(result.is_err(), "Same version should be rejected");
504 }
505
506 #[test]
508 fn test_upgrade_validation_passes() {
509 let upgrader = Upgrader::with_version(Version::new(1, 0, 0));
510
511 let info = UpgradeInfo {
512 version: Version::new(1, 1, 0),
513 download_url: "test".to_string(),
514 signature_url: "test.sig".to_string(),
515 release_notes: "New".to_string(),
516 };
517
518 let result = upgrader.validate_upgrade(&info);
519 assert!(result.is_ok(), "Newer version should be accepted");
520 }
521
522 #[test]
524 fn test_restore_fails_without_backup() {
525 let temp = TempDir::new().unwrap();
526 let current = temp.path().join("binary");
527 let rollback_dir = temp.path().join("rollback");
528 fs::create_dir(&rollback_dir).unwrap();
529
530 fs::write(¤t, b"content").unwrap();
531
532 let upgrader = Upgrader::new();
533 let result = upgrader.restore_from_backup(¤t, &rollback_dir);
534
535 assert!(result.is_err());
536 assert!(result.unwrap_err().to_string().contains("No backup"));
537 }
538
539 #[cfg(unix)]
541 #[test]
542 fn test_permissions_preserved() {
543 use std::os::unix::fs::PermissionsExt;
544
545 let temp = TempDir::new().unwrap();
546 let current = temp.path().join("binary");
547 let new_binary = temp.path().join("new");
548
549 fs::write(¤t, b"old").unwrap();
550 fs::write(&new_binary, b"new").unwrap();
551
552 let mut perms = fs::metadata(¤t).unwrap().permissions();
554 perms.set_mode(0o755);
555 fs::set_permissions(¤t, perms).unwrap();
556
557 let upgrader = Upgrader::new();
558 upgrader.atomic_replace(&new_binary, ¤t).unwrap();
559
560 let new_perms = fs::metadata(¤t).unwrap().permissions();
561 assert_eq!(
562 new_perms.mode() & 0o777,
563 0o755,
564 "Permissions should be preserved"
565 );
566 }
567
568 #[test]
570 fn test_current_version_getter() {
571 let version = Version::new(2, 3, 4);
572 let upgrader = Upgrader::with_version(version.clone());
573 assert_eq!(*upgrader.current_version(), version);
574 }
575
576 #[test]
578 fn test_default_impl() {
579 let upgrader = Upgrader::default();
580 assert!(!upgrader.current_version().to_string().is_empty());
582 }
583
584 #[test]
586 fn test_backup_special_filename() {
587 let temp = TempDir::new().unwrap();
588 let current = temp.path().join("ant-node-v1.0.0");
589 let rollback_dir = temp.path().join("rollback");
590 fs::create_dir(&rollback_dir).unwrap();
591
592 fs::write(¤t, b"content").unwrap();
593
594 let upgrader = Upgrader::new();
595 let result = upgrader.create_backup(¤t, &rollback_dir);
596 assert!(result.is_ok());
597
598 let backup_path = rollback_dir.join("ant-node-v1.0.0.backup");
599 assert!(backup_path.exists());
600 }
601
602 #[test]
604 fn test_upgrade_info() {
605 let info = UpgradeInfo {
606 version: Version::new(1, 2, 3),
607 download_url: "https://example.com/binary".to_string(),
608 signature_url: "https://example.com/binary.sig".to_string(),
609 release_notes: "Bug fixes and improvements".to_string(),
610 };
611
612 assert_eq!(info.version, Version::new(1, 2, 3));
613 assert!(info.download_url.contains("example.com"));
614 assert!(info.signature_url.ends_with(".sig"));
615 }
616
617 #[test]
619 fn test_upgrade_result_variants() {
620 let success = UpgradeResult::Success {
621 version: Version::new(1, 0, 0),
622 exit_code: 0,
623 };
624 assert!(matches!(success, UpgradeResult::Success { .. }));
625
626 let rolled_back = UpgradeResult::RolledBack {
627 reason: "Test failure".to_string(),
628 };
629 assert!(matches!(rolled_back, UpgradeResult::RolledBack { .. }));
630
631 let no_upgrade = UpgradeResult::NoUpgrade;
632 assert!(matches!(no_upgrade, UpgradeResult::NoUpgrade));
633 }
634
635 #[test]
637 fn test_large_file_backup() {
638 let temp = TempDir::new().unwrap();
639 let current = temp.path().join("large_binary");
640 let rollback_dir = temp.path().join("rollback");
641 fs::create_dir(&rollback_dir).unwrap();
642
643 let large_content: Vec<u8> = (0..1_000_000).map(|i| (i % 256) as u8).collect();
645 fs::write(¤t, &large_content).unwrap();
646
647 let upgrader = Upgrader::new();
648 upgrader.create_backup(¤t, &rollback_dir).unwrap();
649
650 let backup_path = rollback_dir.join("large_binary.backup");
651 assert_eq!(fs::read(&backup_path).unwrap(), large_content);
652 }
653
654 #[test]
656 fn test_backup_nonexistent_rollback_dir() {
657 let temp = TempDir::new().unwrap();
658 let current = temp.path().join("binary");
659 let rollback_dir = temp.path().join("nonexistent");
660
661 fs::write(¤t, b"content").unwrap();
662
663 let upgrader = Upgrader::new();
664 let result = upgrader.create_backup(¤t, &rollback_dir);
665
666 assert!(result.is_err(), "Should fail if rollback dir doesn't exist");
667 }
668
669 #[test]
671 fn test_tempdir_in_target_dir() {
672 let temp = TempDir::new().unwrap();
673 let current = temp.path().join("binary");
674 fs::write(¤t, b"content").unwrap();
675
676 let tempdir = Upgrader::create_tempdir_in_target_dir(¤t).unwrap();
677
678 assert_eq!(
679 tempdir.path().parent().unwrap(),
680 temp.path(),
681 "Upgrade tempdir should be in same dir as target"
682 );
683 }
684
685 #[test]
687 fn test_enforce_max_binary_size_rejects_large() {
688 let too_large = MAX_BINARY_SIZE_BYTES + 1;
689 let result = Upgrader::enforce_max_binary_size(too_large);
690 assert!(result.is_err());
691 }
692
693 #[test]
695 fn test_enforce_max_binary_size_accepts_small() {
696 let result = Upgrader::enforce_max_binary_size(1024);
697 assert!(result.is_ok());
698 }
699
700 #[test]
701 fn test_auto_upgrade_supported_on_all_platforms() {
702 assert!(Upgrader::auto_upgrade_supported());
703 }
704}