backup.rs (105550B)
1 //! Consistent SQLite backup capture and bundle layout. 2 3 use std::{ 4 collections::BTreeSet, 5 fs::{self, File}, 6 io::{Read, Write}, 7 path::{Component, Path, PathBuf}, 8 }; 9 10 use radroots_storage::backup::{ 11 BackupCapabilityError, BackupFormatVersion, BackupId, BackupManifest, BackupMember, 12 BackupMemberKind, BackupOperation, BackupPlan, BackupSecretPolicy, BackupTransition, 13 MemberDigest, MemberVerification, ReliabilityRevision, RestoreCapabilityError, 14 RestoreMemberStatus, RestoreOperation, RestorePlan, RestoreTransition, StorageReliability, 15 }; 16 use radroots_storage::status::EventStoreMode; 17 use radroots_storage::{Error as StorageError, outbox::BoxFuture}; 18 use sha2::{Digest, Sha256}; 19 use sqlx::SqlitePool; 20 use sqlx::{Connection, SqliteConnection, sqlite::SqliteConnectOptions}; 21 22 use crate::{Error, OpenMode, SqliteStorage, integrity, migration}; 23 24 const RUNTIME_DATABASE: &str = "runtime.sqlite"; 25 const PRIVATE_DATABASE: &str = "private.sqlite"; 26 const RUNTIME_MEMBER: &str = "runtime/runtime.sqlite"; 27 const PRIVATE_MEMBER: &str = "private/private.sqlite"; 28 const RESTORE_MARKER_MAGIC: &[u8; 8] = b"RDRSTR01"; 29 const RESTORE_MARKER_BYTES: usize = 105; 30 31 mod capability; 32 #[cfg(test)] 33 mod capability_tests; 34 #[cfg(test)] 35 mod restore_capability_tests; 36 mod settling; 37 38 #[derive(Default)] 39 pub(crate) struct ReliabilityState { 40 backups: Vec<BackupOperation>, 41 restores: Vec<RestoreOperation>, 42 } 43 44 impl SqliteStorage { 45 fn reliability_state( 46 &self, 47 ) -> Result<std::sync::MutexGuard<'_, ReliabilityState>, StorageError> { 48 self.lifecycle.require_open()?; 49 self.reliability 50 .lock() 51 .map_err(|_| StorageError::BackendUnavailable) 52 } 53 } 54 55 impl StorageReliability for SqliteStorage { 56 fn settle_backup_writes(&self) -> BoxFuture<'_, Result<(), BackupCapabilityError>> { 57 Box::pin(settling::settle(self)) 58 } 59 60 fn capture_backup( 61 &self, 62 plan: BackupPlan, 63 ) -> BoxFuture<'_, Result<BackupManifest, BackupCapabilityError>> { 64 Box::pin(async move { 65 SqliteStorage::capture_backup(self, &plan) 66 .await 67 .map_err(capability::map_error) 68 }) 69 } 70 71 fn verify_backup( 72 &self, 73 plan: BackupPlan, 74 manifest: BackupManifest, 75 ) -> BoxFuture<'_, Result<(), BackupCapabilityError>> { 76 Box::pin(async move { 77 SqliteStorage::verify_backup(self, &plan, &manifest) 78 .await 79 .map_err(capability::map_error) 80 }) 81 } 82 83 fn finalize_backup( 84 &self, 85 plan: BackupPlan, 86 manifest: BackupManifest, 87 ) -> BoxFuture<'_, Result<(), BackupCapabilityError>> { 88 Box::pin(async move { 89 SqliteStorage::finalize_backup(self, &plan, &manifest) 90 .await 91 .map(|_| ()) 92 .map_err(capability::map_error) 93 }) 94 } 95 96 fn stage_restore( 97 &self, 98 plan: RestorePlan, 99 ) -> BoxFuture<'_, Result<Vec<RestoreMemberStatus>, RestoreCapabilityError>> { 100 Box::pin(async move { 101 SqliteStorage::stage_restore(self, &plan) 102 .await 103 .map_err(capability::map_restore_error) 104 }) 105 } 106 107 fn finalize_restore( 108 &self, 109 plan: RestorePlan, 110 ) -> BoxFuture<'_, Result<(), RestoreCapabilityError>> { 111 Box::pin(async move { 112 SqliteStorage::finalize_restore(self, &plan) 113 .await 114 .map_err(capability::map_restore_error) 115 }) 116 } 117 118 fn begin_backup( 119 &self, 120 plan: BackupPlan, 121 ) -> BoxFuture<'_, Result<BackupOperation, StorageError>> { 122 Box::pin(async move { 123 let mut state = self.reliability_state()?; 124 if let Some(existing) = state 125 .backups 126 .iter() 127 .find(|operation| operation.plan().backup_id() == plan.backup_id()) 128 { 129 return if existing.plan() == &plan { 130 Ok(existing.clone()) 131 } else { 132 Err(StorageError::ReliabilityRevisionConflict) 133 }; 134 } 135 let operation = BackupOperation::planned(plan); 136 state.backups.push(operation.clone()); 137 Ok(operation) 138 }) 139 } 140 141 fn transition_backup( 142 &self, 143 backup_id: BackupId, 144 expected_revision: ReliabilityRevision, 145 transition: BackupTransition, 146 at_unix_ms: u64, 147 ) -> BoxFuture<'_, Result<BackupOperation, StorageError>> { 148 Box::pin(async move { 149 let mut state = self.reliability_state()?; 150 let operation = state 151 .backups 152 .iter_mut() 153 .find(|operation| operation.plan().backup_id() == backup_id) 154 .ok_or(StorageError::CorruptReliabilityOperation)?; 155 let next = operation.transition(expected_revision, transition, at_unix_ms)?; 156 *operation = next.clone(); 157 Ok(next) 158 }) 159 } 160 161 fn begin_restore( 162 &self, 163 plan: RestorePlan, 164 ) -> BoxFuture<'_, Result<RestoreOperation, StorageError>> { 165 Box::pin(async move { 166 let mut state = self.reliability_state()?; 167 let backup_id = plan.manifest().backup_id(); 168 if let Some(existing) = state 169 .restores 170 .iter() 171 .find(|operation| operation.plan().manifest().backup_id() == backup_id) 172 { 173 return if existing.plan() == &plan { 174 Ok(existing.clone()) 175 } else { 176 Err(StorageError::ReliabilityRevisionConflict) 177 }; 178 } 179 let operation = RestoreOperation::staging(plan); 180 state.restores.push(operation.clone()); 181 Ok(operation) 182 }) 183 } 184 185 fn transition_restore( 186 &self, 187 backup_id: BackupId, 188 expected_revision: ReliabilityRevision, 189 transition: RestoreTransition, 190 at_unix_ms: u64, 191 ) -> BoxFuture<'_, Result<RestoreOperation, StorageError>> { 192 Box::pin(async move { 193 let mut state = self.reliability_state()?; 194 let operation = state 195 .restores 196 .iter_mut() 197 .find(|operation| operation.plan().manifest().backup_id() == backup_id) 198 .ok_or(StorageError::CorruptReliabilityOperation)?; 199 let next = operation.transition(expected_revision, transition, at_unix_ms)?; 200 *operation = next.clone(); 201 Ok(next) 202 }) 203 } 204 205 fn integrity( 206 &self, 207 ) -> BoxFuture<'_, Result<radroots_storage::status::IntegrityStatus, StorageError>> { 208 Box::pin(async move { SqliteStorage::integrity(self).await }) 209 } 210 211 fn status(&self) -> BoxFuture<'_, Result<radroots_storage::StorageStatus, StorageError>> { 212 Box::pin(async move { SqliteStorage::storage_status(self).await }) 213 } 214 215 fn close(&self) -> BoxFuture<'_, Result<radroots_storage::StorageStatus, StorageError>> { 216 Box::pin(async move { SqliteStorage::close(self).await }) 217 } 218 } 219 220 impl SqliteStorage { 221 /// Captures consistent SQLite snapshots into a new deterministic staging 222 /// bundle under the configured host-owned backup root. 223 #[cfg_attr(coverage_nightly, coverage(off))] 224 pub async fn capture_backup(&self, plan: &BackupPlan) -> Result<BackupManifest, Error> { 225 self.lifecycle 226 .require_open() 227 .map_err(|_| Error::BackupBackendUnavailable)?; 228 if plan.format_version() != BackupFormatVersion::V1 { 229 return Err(Error::UnsupportedBackupVersion); 230 } 231 let backup_root = self 232 .backup_root 233 .as_deref() 234 .ok_or(Error::BackupRootRequired)?; 235 validate_backup_root(backup_root)?; 236 let layout = BackupLayout::new(backup_root, plan); 237 layout.create(plan.secret_policy())?; 238 239 let mut members = Vec::with_capacity( 240 if plan.secret_policy() == BackupSecretPolicy::IncludeProtectedStorage { 241 2 242 } else { 243 1 244 }, 245 ); 246 members.push( 247 capture_member( 248 &self.pool, 249 &layout.runtime_file, 250 RUNTIME_MEMBER, 251 BackupMemberKind::Runtime, 252 ) 253 .await?, 254 ); 255 sync_directory(&layout.runtime_directory, "sync runtime member directory")?; 256 257 if plan.secret_policy() == BackupSecretPolicy::IncludeProtectedStorage { 258 members.push( 259 capture_member( 260 &self.private_pool, 261 &layout.private_file, 262 PRIVATE_MEMBER, 263 BackupMemberKind::Protected, 264 ) 265 .await?, 266 ); 267 sync_directory(&layout.private_directory, "sync private member directory")?; 268 } 269 sync_directory(&layout.staging, "sync staging bundle directory")?; 270 sync_directory(backup_root, "sync backup root")?; 271 272 BackupManifest::new( 273 plan.format_version(), 274 plan.backup_id(), 275 plan.requested_at_unix_ms(), 276 plan.secret_policy(), 277 members, 278 ) 279 .map_err(|_| Error::BackupCaptureFailed { member: "manifest" }) 280 } 281 282 /// Verifies the complete staged bundle without mutating or finalizing it. 283 #[cfg_attr(coverage_nightly, coverage(off))] 284 pub async fn verify_backup( 285 &self, 286 plan: &BackupPlan, 287 manifest: &BackupManifest, 288 ) -> Result<(), Error> { 289 self.lifecycle 290 .require_open() 291 .map_err(|_| Error::BackupBackendUnavailable)?; 292 let backup_root = self 293 .backup_root 294 .as_deref() 295 .ok_or(Error::BackupRootRequired)?; 296 validate_backup_root(backup_root)?; 297 let layout = BackupLayout::new(backup_root, plan); 298 verify_bundle(&layout.staging, plan, manifest).await 299 } 300 301 /// Verifies and atomically renames a complete staging bundle. A retry 302 /// against an already finalized valid bundle succeeds idempotently. 303 #[cfg_attr(coverage_nightly, coverage(off))] 304 pub async fn finalize_backup( 305 &self, 306 plan: &BackupPlan, 307 manifest: &BackupManifest, 308 ) -> Result<PathBuf, Error> { 309 self.lifecycle 310 .require_open() 311 .map_err(|_| Error::BackupBackendUnavailable)?; 312 let backup_root = self 313 .backup_root 314 .as_deref() 315 .ok_or(Error::BackupRootRequired)?; 316 validate_backup_root(backup_root)?; 317 let layout = BackupLayout::new(backup_root, plan); 318 let staging = entry_kind(&layout.staging)?; 319 let finalized = entry_kind(&layout.finalized)?; 320 match (staging, finalized) { 321 (EntryKind::Missing, EntryKind::Directory) => { 322 verify_bundle(&layout.finalized, plan, manifest).await?; 323 Ok(layout.finalized) 324 } 325 (EntryKind::Directory, EntryKind::Missing) => { 326 verify_bundle(&layout.staging, plan, manifest).await?; 327 fs::rename(&layout.staging, &layout.finalized).map_err(|source| { 328 Error::BackupFilesystem { 329 operation: "atomically finalize backup bundle", 330 source, 331 } 332 })?; 333 sync_directory(backup_root, "sync finalized backup root")?; 334 Ok(layout.finalized) 335 } 336 (EntryKind::Missing, EntryKind::Missing) => { 337 Err(Error::BackupBundleMissing(layout.staging)) 338 } 339 (_, EntryKind::Directory) => Err(Error::BackupBundleAlreadyExists(layout.finalized)), 340 (EntryKind::Other, _) => Err(Error::BackupUnexpectedEntry(layout.staging)), 341 (_, EntryKind::Other) => Err(Error::BackupUnexpectedEntry(layout.finalized)), 342 } 343 } 344 345 /// Copies a verified finalized bundle into create-new files adjacent to 346 /// the live databases and verifies every staged copy before replacement. 347 #[cfg_attr(coverage_nightly, coverage(off))] 348 pub async fn stage_restore( 349 &self, 350 plan: &RestorePlan, 351 ) -> Result<Vec<RestoreMemberStatus>, Error> { 352 self.lifecycle 353 .require_open() 354 .map_err(|_| Error::BackupBackendUnavailable)?; 355 if self.mode != EventStoreMode::ReadWrite { 356 return Err(Error::RestoreRequiresWritableStorage); 357 } 358 let backup_root = self 359 .backup_root 360 .as_deref() 361 .ok_or(Error::BackupRootRequired)?; 362 let live_paths = self 363 .paths 364 .as_deref() 365 .ok_or(Error::BackupBackendUnavailable)?; 366 validate_backup_root(backup_root)?; 367 let manifest = plan.manifest(); 368 let backup_plan = BackupPlan::new( 369 manifest.backup_id(), 370 manifest.format_version(), 371 manifest.secret_policy(), 372 manifest.created_at_unix_ms(), 373 ) 374 .map_err(|_| Error::RestoreStagingFailed { member: "manifest" })?; 375 let bundle = BackupLayout::new(backup_root, &backup_plan).finalized; 376 verify_bundle(&bundle, &backup_plan, manifest).await?; 377 let staging = RestoreStaging::new(live_paths, manifest)?; 378 staging.require_absent(manifest.secret_policy())?; 379 380 copy_staged_member( 381 &bundle.join(RUNTIME_MEMBER), 382 &staging.runtime, 383 manifest 384 .member(RUNTIME_MEMBER) 385 .ok_or(Error::RestoreStagingFailed { 386 member: RUNTIME_MEMBER, 387 })?, 388 BackupMemberKind::Runtime, 389 RUNTIME_MEMBER, 390 true, 391 ) 392 .await?; 393 sync_parent(&staging.runtime, "sync runtime restore parent")?; 394 let mut statuses = vec![ 395 RestoreMemberStatus::new(RUNTIME_MEMBER, MemberVerification::Verified).map_err( 396 |_| Error::RestoreStagingFailed { 397 member: RUNTIME_MEMBER, 398 }, 399 )?, 400 ]; 401 402 if manifest.secret_policy() == BackupSecretPolicy::IncludeProtectedStorage { 403 copy_staged_member( 404 &bundle.join(PRIVATE_MEMBER), 405 &staging.private, 406 manifest 407 .member(PRIVATE_MEMBER) 408 .ok_or(Error::RestoreStagingFailed { 409 member: PRIVATE_MEMBER, 410 })?, 411 BackupMemberKind::Protected, 412 PRIVATE_MEMBER, 413 false, 414 ) 415 .await?; 416 sync_parent(&staging.private, "sync private restore parent")?; 417 statuses.push( 418 RestoreMemberStatus::new(PRIVATE_MEMBER, MemberVerification::Verified).map_err( 419 |_| Error::RestoreStagingFailed { 420 member: PRIVATE_MEMBER, 421 }, 422 )?, 423 ); 424 } 425 Ok(statuses) 426 } 427 428 /// Quiesces this writable backend, records a durable interruption marker, 429 /// and installs every completely verified staged member. The backend is 430 /// closed after the attempt and must be reopened to observe restored state. 431 #[cfg_attr(coverage_nightly, coverage(off))] 432 pub async fn finalize_restore(&self, plan: &RestorePlan) -> Result<(), Error> { 433 self.lifecycle 434 .require_open() 435 .map_err(|_| Error::BackupBackendUnavailable)?; 436 if self.mode != EventStoreMode::ReadWrite { 437 return Err(Error::RestoreRequiresWritableStorage); 438 } 439 let paths = self 440 .paths 441 .as_deref() 442 .ok_or(Error::BackupBackendUnavailable)?; 443 let marker = RestoreMarker::from_manifest(plan.manifest())?; 444 let layout = RestoreLayout::new(paths, marker.backup_id())?; 445 verify_staged_restore(&layout, &marker).await?; 446 layout.require_previous_absent(marker.secret_policy())?; 447 448 let restoration = self 449 .lifecycle 450 .begin_restore_close() 451 .map_err(|_| Error::BackupBackendUnavailable)?; 452 self.pool.close().await; 453 self.private_pool.close().await; 454 let installation = async { 455 verify_staged_restore(&layout, &marker).await?; 456 write_restore_marker(&layout.marker, &marker)?; 457 recover_interrupted_restore(paths, OpenMode::ReadWriteExisting).await 458 } 459 .await; 460 let close = restoration.finish(); 461 installation?; 462 close.map_err(|_| Error::BackupBackendUnavailable) 463 } 464 } 465 466 #[cfg_attr(coverage_nightly, coverage(off))] 467 pub(crate) fn validate_backup_root(path: &Path) -> Result<(), Error> { 468 if !path.is_absolute() 469 || path.to_str().is_none() 470 || path 471 .components() 472 .any(|component| matches!(component, Component::CurDir | Component::ParentDir)) 473 { 474 return Err(Error::InvalidBackupRoot(path.to_path_buf())); 475 } 476 match fs::symlink_metadata(path) { 477 Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => { 478 Err(Error::InvalidBackupRoot(path.to_path_buf())) 479 } 480 Ok(_) => Ok(()), 481 Err(source) if source.kind() == std::io::ErrorKind::NotFound => { 482 Err(Error::InvalidBackupRoot(path.to_path_buf())) 483 } 484 Err(source) => Err(Error::BackupFilesystem { 485 operation: "inspect backup root", 486 source, 487 }), 488 } 489 } 490 491 struct BackupLayout { 492 staging: PathBuf, 493 finalized: PathBuf, 494 runtime_directory: PathBuf, 495 private_directory: PathBuf, 496 runtime_file: PathBuf, 497 private_file: PathBuf, 498 } 499 500 #[derive(Clone, Copy, Eq, PartialEq)] 501 enum EntryKind { 502 Missing, 503 Directory, 504 Other, 505 } 506 507 #[cfg_attr(coverage_nightly, coverage(off))] 508 fn entry_kind(path: &Path) -> Result<EntryKind, Error> { 509 match fs::symlink_metadata(path) { 510 Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => { 511 Ok(EntryKind::Directory) 512 } 513 Ok(_) => Ok(EntryKind::Other), 514 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(EntryKind::Missing), 515 Err(source) => Err(Error::BackupFilesystem { 516 operation: "inspect backup bundle", 517 source, 518 }), 519 } 520 } 521 522 impl BackupLayout { 523 fn new(root: &Path, plan: &BackupPlan) -> Self { 524 let id = encode_backup_id(plan); 525 let staging = root.join(format!(".radroots-backup-{id}.staging")); 526 let finalized = root.join(format!("radroots-backup-{id}")); 527 let runtime_directory = staging.join("runtime"); 528 let private_directory = staging.join("private"); 529 let runtime_file = runtime_directory.join(RUNTIME_DATABASE); 530 let private_file = private_directory.join(PRIVATE_DATABASE); 531 Self { 532 staging, 533 finalized, 534 runtime_directory, 535 private_directory, 536 runtime_file, 537 private_file, 538 } 539 } 540 541 #[cfg_attr(coverage_nightly, coverage(off))] 542 fn create(&self, secret_policy: BackupSecretPolicy) -> Result<(), Error> { 543 for path in [&self.staging, &self.finalized] { 544 if path 545 .try_exists() 546 .map_err(|source| Error::BackupFilesystem { 547 operation: "inspect bundle path", 548 source, 549 })? 550 { 551 return Err(Error::BackupBundleAlreadyExists(path.clone())); 552 } 553 } 554 create_private_directory(&self.staging, "create staging bundle")?; 555 create_private_directory(&self.runtime_directory, "create runtime member directory")?; 556 if secret_policy == BackupSecretPolicy::IncludeProtectedStorage { 557 create_private_directory(&self.private_directory, "create private member directory")?; 558 } 559 Ok(()) 560 } 561 } 562 563 fn encode_backup_id(plan: &BackupPlan) -> String { 564 encode_id(plan.backup_id().as_bytes()) 565 } 566 567 fn encode_id(bytes: &[u8; 16]) -> String { 568 const HEX: &[u8; 16] = b"0123456789abcdef"; 569 let mut encoded = String::with_capacity(32); 570 for byte in bytes { 571 encoded.push(char::from(HEX[usize::from(byte >> 4)])); 572 encoded.push(char::from(HEX[usize::from(byte & 0x0f)])); 573 } 574 encoded 575 } 576 577 struct RestoreStaging { 578 runtime: PathBuf, 579 private: PathBuf, 580 } 581 582 impl RestoreStaging { 583 fn new(paths: &crate::Paths, manifest: &BackupManifest) -> Result<Self, Error> { 584 let id = encode_id(manifest.backup_id().as_bytes()); 585 Ok(Self { 586 runtime: staged_restore_path(paths.runtime(), &id)?, 587 private: staged_restore_path(paths.private(), &id)?, 588 }) 589 } 590 591 #[cfg_attr(coverage_nightly, coverage(off))] 592 fn require_absent(&self, policy: BackupSecretPolicy) -> Result<(), Error> { 593 let paths = if policy == BackupSecretPolicy::IncludeProtectedStorage { 594 vec![&self.runtime, &self.private] 595 } else { 596 vec![&self.runtime] 597 }; 598 for path in paths { 599 if entry_kind(path)? != EntryKind::Missing { 600 return Err(Error::RestoreStagingAlreadyExists(path.clone())); 601 } 602 } 603 Ok(()) 604 } 605 } 606 607 struct RestoreLayout { 608 runtime_live: PathBuf, 609 private_live: PathBuf, 610 runtime_staging: PathBuf, 611 private_staging: PathBuf, 612 runtime_previous: PathBuf, 613 private_previous: PathBuf, 614 marker: PathBuf, 615 } 616 617 impl RestoreLayout { 618 fn new(paths: &crate::Paths, backup_id: BackupId) -> Result<Self, Error> { 619 let id = encode_id(backup_id.as_bytes()); 620 let runtime_parent = paths 621 .runtime() 622 .parent() 623 .ok_or_else(|| Error::InvalidPath(paths.runtime().to_path_buf()))?; 624 Ok(Self { 625 runtime_live: paths.runtime().to_path_buf(), 626 private_live: paths.private().to_path_buf(), 627 runtime_staging: restore_sidecar_path(paths.runtime(), &id, "staging")?, 628 private_staging: restore_sidecar_path(paths.private(), &id, "staging")?, 629 runtime_previous: restore_sidecar_path(paths.runtime(), &id, "previous")?, 630 private_previous: restore_sidecar_path(paths.private(), &id, "previous")?, 631 marker: runtime_parent.join(format!(".radroots-storage-restore-{id}.marker")), 632 }) 633 } 634 635 #[cfg_attr(coverage_nightly, coverage(off))] 636 fn require_previous_absent(&self, policy: BackupSecretPolicy) -> Result<(), Error> { 637 let paths = if policy == BackupSecretPolicy::IncludeProtectedStorage { 638 vec![&self.runtime_previous, &self.private_previous] 639 } else { 640 vec![&self.runtime_previous] 641 }; 642 for path in paths { 643 if restore_entry_kind(path)? != RestoreEntryKind::Missing { 644 return Err(Error::RestoreRecoveryConflict(path.clone())); 645 } 646 } 647 if restore_entry_kind(&self.marker)? != RestoreEntryKind::Missing { 648 return Err(Error::RestoreRecoveryConflict(self.marker.clone())); 649 } 650 Ok(()) 651 } 652 } 653 654 fn staged_restore_path(live: &Path, id: &str) -> Result<PathBuf, Error> { 655 restore_sidecar_path(live, id, "staging") 656 } 657 658 fn restore_sidecar_path(live: &Path, id: &str, role: &str) -> Result<PathBuf, Error> { 659 let name = live 660 .file_name() 661 .and_then(|value| value.to_str()) 662 .ok_or_else(|| Error::InvalidPath(live.to_path_buf()))?; 663 Ok(live.with_file_name(format!(".{name}.restore-{id}.{role}"))) 664 } 665 666 #[derive(Clone, Copy)] 667 struct RestoreMemberExpectation { 668 byte_length: u64, 669 sha256: MemberDigest, 670 } 671 672 impl RestoreMemberExpectation { 673 fn from_member(member: &BackupMember, expected_kind: BackupMemberKind) -> Result<Self, Error> { 674 if member.kind() != expected_kind { 675 return Err(Error::RestoreReplacementFailed { member: "manifest" }); 676 } 677 Ok(Self { 678 byte_length: member.byte_length(), 679 sha256: member.sha256(), 680 }) 681 } 682 683 fn member( 684 self, 685 relative_path: &'static str, 686 kind: BackupMemberKind, 687 ) -> Result<BackupMember, Error> { 688 BackupMember::new(relative_path, kind, self.byte_length, self.sha256).map_err(|_| { 689 Error::RestoreReplacementFailed { 690 member: relative_path, 691 } 692 }) 693 } 694 } 695 696 struct RestoreMarker { 697 backup_id: BackupId, 698 secret_policy: BackupSecretPolicy, 699 runtime: RestoreMemberExpectation, 700 private: Option<RestoreMemberExpectation>, 701 } 702 703 impl RestoreMarker { 704 fn from_manifest(manifest: &BackupManifest) -> Result<Self, Error> { 705 if manifest.format_version() != BackupFormatVersion::V1 { 706 return Err(Error::UnsupportedBackupVersion); 707 } 708 let expected = if manifest.secret_policy() == BackupSecretPolicy::IncludeProtectedStorage { 709 BTreeSet::from([PRIVATE_MEMBER, RUNTIME_MEMBER]) 710 } else { 711 BTreeSet::from([RUNTIME_MEMBER]) 712 }; 713 let actual = manifest 714 .members() 715 .iter() 716 .map(BackupMember::relative_path) 717 .collect::<BTreeSet<_>>(); 718 if actual != expected { 719 return Err(Error::RestoreReplacementFailed { member: "manifest" }); 720 } 721 let runtime = RestoreMemberExpectation::from_member( 722 manifest 723 .member(RUNTIME_MEMBER) 724 .ok_or(Error::RestoreReplacementFailed { member: "manifest" })?, 725 BackupMemberKind::Runtime, 726 )?; 727 let private = manifest 728 .member(PRIVATE_MEMBER) 729 .map(|member| { 730 RestoreMemberExpectation::from_member(member, BackupMemberKind::Protected) 731 }) 732 .transpose()?; 733 Ok(Self { 734 backup_id: manifest.backup_id(), 735 secret_policy: manifest.secret_policy(), 736 runtime, 737 private, 738 }) 739 } 740 741 const fn backup_id(&self) -> BackupId { 742 self.backup_id 743 } 744 745 const fn secret_policy(&self) -> BackupSecretPolicy { 746 self.secret_policy 747 } 748 749 fn encode(&self) -> [u8; RESTORE_MARKER_BYTES] { 750 let mut encoded = [0_u8; RESTORE_MARKER_BYTES]; 751 encoded[..8].copy_from_slice(RESTORE_MARKER_MAGIC); 752 encoded[8] = u8::from(self.private.is_some()); 753 encoded[9..25].copy_from_slice(self.backup_id.as_bytes()); 754 encoded[25..33].copy_from_slice(&self.runtime.byte_length.to_be_bytes()); 755 encoded[33..65].copy_from_slice(self.runtime.sha256.as_bytes()); 756 if let Some(private) = self.private { 757 encoded[65..73].copy_from_slice(&private.byte_length.to_be_bytes()); 758 encoded[73..105].copy_from_slice(private.sha256.as_bytes()); 759 } 760 encoded 761 } 762 763 fn decode(path: &Path, encoded: &[u8]) -> Result<Self, Error> { 764 if encoded.len() != RESTORE_MARKER_BYTES || &encoded[..8] != RESTORE_MARKER_MAGIC { 765 return Err(Error::RestoreMarkerCorrupt(path.to_path_buf())); 766 } 767 let secret_policy = match encoded[8] { 768 0 => BackupSecretPolicy::ExcludeProtectedStorage, 769 1 => BackupSecretPolicy::IncludeProtectedStorage, 770 _ => return Err(Error::RestoreMarkerCorrupt(path.to_path_buf())), 771 }; 772 let backup_id = BackupId::new( 773 encoded[9..25] 774 .try_into() 775 .map_err(|_| Error::RestoreMarkerCorrupt(path.to_path_buf()))?, 776 ) 777 .map_err(|_| Error::RestoreMarkerCorrupt(path.to_path_buf()))?; 778 let runtime = RestoreMemberExpectation { 779 byte_length: u64::from_be_bytes( 780 encoded[25..33] 781 .try_into() 782 .map_err(|_| Error::RestoreMarkerCorrupt(path.to_path_buf()))?, 783 ), 784 sha256: MemberDigest::new( 785 encoded[33..65] 786 .try_into() 787 .map_err(|_| Error::RestoreMarkerCorrupt(path.to_path_buf()))?, 788 ), 789 }; 790 if runtime.byte_length == 0 { 791 return Err(Error::RestoreMarkerCorrupt(path.to_path_buf())); 792 } 793 let private_length = u64::from_be_bytes( 794 encoded[65..73] 795 .try_into() 796 .map_err(|_| Error::RestoreMarkerCorrupt(path.to_path_buf()))?, 797 ); 798 let private_digest: [u8; 32] = encoded[73..105] 799 .try_into() 800 .map_err(|_| Error::RestoreMarkerCorrupt(path.to_path_buf()))?; 801 let private = match secret_policy { 802 BackupSecretPolicy::ExcludeProtectedStorage 803 if private_length == 0 && private_digest == [0; 32] => 804 { 805 None 806 } 807 BackupSecretPolicy::IncludeProtectedStorage if private_length > 0 => { 808 Some(RestoreMemberExpectation { 809 byte_length: private_length, 810 sha256: MemberDigest::new(private_digest), 811 }) 812 } 813 _ => return Err(Error::RestoreMarkerCorrupt(path.to_path_buf())), 814 }; 815 Ok(Self { 816 backup_id, 817 secret_policy, 818 runtime, 819 private, 820 }) 821 } 822 } 823 824 #[cfg_attr(coverage_nightly, coverage(off))] 825 fn write_restore_marker(path: &Path, marker: &RestoreMarker) -> Result<(), Error> { 826 let mut options = fs::OpenOptions::new(); 827 options.create_new(true).write(true); 828 #[cfg(unix)] 829 { 830 use std::os::unix::fs::OpenOptionsExt; 831 options.mode(0o600); 832 } 833 let mut file = options 834 .open(path) 835 .map_err(|source| Error::RestoreFilesystem { 836 operation: "create restore interruption marker", 837 source, 838 })?; 839 file.write_all(&marker.encode()) 840 .and_then(|()| file.sync_all()) 841 .map_err(|source| Error::RestoreFilesystem { 842 operation: "persist restore interruption marker", 843 source, 844 })?; 845 sync_parent(path, "sync restore marker parent") 846 } 847 848 #[cfg_attr(coverage_nightly, coverage(off))] 849 fn read_restore_marker(path: &Path) -> Result<RestoreMarker, Error> { 850 let metadata = fs::symlink_metadata(path).map_err(|source| Error::RestoreFilesystem { 851 operation: "inspect restore interruption marker", 852 source, 853 })?; 854 if metadata.file_type().is_symlink() 855 || !metadata.is_file() 856 || metadata.len() != RESTORE_MARKER_BYTES as u64 857 { 858 return Err(Error::RestoreMarkerCorrupt(path.to_path_buf())); 859 } 860 let encoded = fs::read(path).map_err(|source| Error::RestoreFilesystem { 861 operation: "read restore interruption marker", 862 source, 863 })?; 864 RestoreMarker::decode(path, &encoded) 865 } 866 867 #[cfg_attr(coverage_nightly, coverage(off))] 868 pub(crate) async fn recover_interrupted_restore( 869 paths: &crate::Paths, 870 mode: OpenMode, 871 ) -> Result<(), Error> { 872 let Some(marker_path) = discover_restore_marker(paths)? else { 873 return Ok(()); 874 }; 875 if !mode.is_writable() { 876 return Err(Error::RestoreRequiresWritableStorage); 877 } 878 let marker = read_restore_marker(&marker_path)?; 879 let layout = RestoreLayout::new(paths, marker.backup_id())?; 880 if layout.marker != marker_path { 881 return Err(Error::RestoreMarkerCorrupt(marker_path)); 882 } 883 require_sqlite_sidecars_absent(paths)?; 884 install_restore_member( 885 &layout.runtime_live, 886 &layout.runtime_staging, 887 &layout.runtime_previous, 888 marker.runtime, 889 BackupMemberKind::Runtime, 890 RUNTIME_MEMBER, 891 true, 892 ) 893 .await?; 894 if let Some(private) = marker.private { 895 install_restore_member( 896 &layout.private_live, 897 &layout.private_staging, 898 &layout.private_previous, 899 private, 900 BackupMemberKind::Protected, 901 PRIVATE_MEMBER, 902 false, 903 ) 904 .await?; 905 } 906 verify_installed_restore(&layout, &marker).await?; 907 remove_restore_file(&layout.runtime_previous, "remove previous runtime database")?; 908 if marker.private.is_some() { 909 remove_restore_file(&layout.private_previous, "remove previous private database")?; 910 } 911 remove_restore_file(&layout.marker, "remove restore interruption marker")?; 912 Ok(()) 913 } 914 915 #[cfg_attr(coverage_nightly, coverage(off))] 916 fn discover_restore_marker(paths: &crate::Paths) -> Result<Option<PathBuf>, Error> { 917 let parent = paths 918 .runtime() 919 .parent() 920 .ok_or_else(|| Error::InvalidPath(paths.runtime().to_path_buf()))?; 921 let parent_metadata = match fs::symlink_metadata(parent) { 922 Ok(metadata) => metadata, 923 Err(source) if source.kind() == std::io::ErrorKind::NotFound => return Ok(None), 924 Err(source) => { 925 return Err(Error::RestoreFilesystem { 926 operation: "inspect restore marker parent", 927 source, 928 }); 929 } 930 }; 931 if parent_metadata.file_type().is_symlink() || !parent_metadata.is_dir() { 932 return Ok(None); 933 } 934 let mut marker = None; 935 for entry in fs::read_dir(parent).map_err(|source| Error::RestoreFilesystem { 936 operation: "scan restore interruption markers", 937 source, 938 })? { 939 let entry = entry.map_err(|source| Error::RestoreFilesystem { 940 operation: "read restore interruption marker entry", 941 source, 942 })?; 943 let Some(name) = entry.file_name().to_str().map(str::to_owned) else { 944 continue; 945 }; 946 if !name.starts_with(".radroots-storage-restore-") || !name.ends_with(".marker") { 947 continue; 948 } 949 if marker.replace(entry.path()).is_some() { 950 return Err(Error::RestoreRecoveryConflict(parent.to_path_buf())); 951 } 952 } 953 Ok(marker) 954 } 955 956 #[cfg_attr(coverage_nightly, coverage(off))] 957 async fn verify_staged_restore( 958 layout: &RestoreLayout, 959 marker: &RestoreMarker, 960 ) -> Result<(), Error> { 961 verify_restore_path( 962 &layout.runtime_staging, 963 marker.runtime, 964 BackupMemberKind::Runtime, 965 RUNTIME_MEMBER, 966 true, 967 ) 968 .await?; 969 if let Some(private) = marker.private { 970 verify_restore_path( 971 &layout.private_staging, 972 private, 973 BackupMemberKind::Protected, 974 PRIVATE_MEMBER, 975 false, 976 ) 977 .await?; 978 } 979 Ok(()) 980 } 981 982 #[cfg_attr(coverage_nightly, coverage(off))] 983 async fn verify_installed_restore( 984 layout: &RestoreLayout, 985 marker: &RestoreMarker, 986 ) -> Result<(), Error> { 987 verify_restore_path( 988 &layout.runtime_live, 989 marker.runtime, 990 BackupMemberKind::Runtime, 991 RUNTIME_MEMBER, 992 true, 993 ) 994 .await?; 995 if let Some(private) = marker.private { 996 verify_restore_path( 997 &layout.private_live, 998 private, 999 BackupMemberKind::Protected, 1000 PRIVATE_MEMBER, 1001 false, 1002 ) 1003 .await?; 1004 } 1005 Ok(()) 1006 } 1007 1008 #[cfg_attr(coverage_nightly, coverage(off))] 1009 async fn verify_restore_path( 1010 path: &Path, 1011 expected: RestoreMemberExpectation, 1012 kind: BackupMemberKind, 1013 member_name: &'static str, 1014 runtime: bool, 1015 ) -> Result<(), Error> { 1016 let member = expected.member(member_name, kind)?; 1017 verify_member(path, &member, kind, member_name, runtime) 1018 .await 1019 .map_err(|_| Error::RestoreReplacementFailed { 1020 member: member_name, 1021 }) 1022 } 1023 1024 #[cfg_attr(coverage_nightly, coverage(off))] 1025 async fn install_restore_member( 1026 live: &Path, 1027 staging: &Path, 1028 previous: &Path, 1029 expected: RestoreMemberExpectation, 1030 kind: BackupMemberKind, 1031 member_name: &'static str, 1032 runtime: bool, 1033 ) -> Result<(), Error> { 1034 let live_kind = restore_entry_kind(live)?; 1035 let staging_kind = restore_entry_kind(staging)?; 1036 let previous_kind = restore_entry_kind(previous)?; 1037 if [live_kind, staging_kind, previous_kind] 1038 .into_iter() 1039 .any(|entry| entry == RestoreEntryKind::Other) 1040 { 1041 return Err(Error::RestoreRecoveryConflict(live.to_path_buf())); 1042 } 1043 1044 if live_kind == RestoreEntryKind::File && restore_member_matches(live, expected)? { 1045 verify_restore_path(live, expected, kind, member_name, runtime).await?; 1046 if staging_kind == RestoreEntryKind::File { 1047 verify_restore_path(staging, expected, kind, member_name, runtime).await?; 1048 remove_restore_file(staging, "remove redundant restore staging member")?; 1049 } 1050 return Ok(()); 1051 } 1052 if staging_kind != RestoreEntryKind::File { 1053 return Err(Error::RestoreReplacementFailed { 1054 member: member_name, 1055 }); 1056 } 1057 verify_restore_path(staging, expected, kind, member_name, runtime).await?; 1058 match (live_kind, previous_kind) { 1059 (RestoreEntryKind::File, RestoreEntryKind::Missing) => { 1060 fs::rename(live, previous).map_err(|source| Error::RestoreFilesystem { 1061 operation: "rename live database to previous restore sidecar", 1062 source, 1063 })?; 1064 sync_parent(live, "sync previous database rename")?; 1065 } 1066 (RestoreEntryKind::Missing, RestoreEntryKind::File) => {} 1067 _ => return Err(Error::RestoreRecoveryConflict(live.to_path_buf())), 1068 } 1069 fs::rename(staging, live).map_err(|source| Error::RestoreFilesystem { 1070 operation: "rename staged restore member into live path", 1071 source, 1072 })?; 1073 sync_parent(live, "sync live restore replacement")?; 1074 verify_restore_path(live, expected, kind, member_name, runtime).await 1075 } 1076 1077 #[cfg_attr(coverage_nightly, coverage(off))] 1078 fn restore_member_matches(path: &Path, expected: RestoreMemberExpectation) -> Result<bool, Error> { 1079 let (length, digest) = fingerprint(path)?; 1080 Ok(length == expected.byte_length && digest == expected.sha256) 1081 } 1082 1083 #[derive(Clone, Copy, Eq, PartialEq)] 1084 enum RestoreEntryKind { 1085 Missing, 1086 File, 1087 Other, 1088 } 1089 1090 #[cfg_attr(coverage_nightly, coverage(off))] 1091 fn restore_entry_kind(path: &Path) -> Result<RestoreEntryKind, Error> { 1092 match fs::symlink_metadata(path) { 1093 Ok(metadata) if metadata.is_file() && !metadata.file_type().is_symlink() => { 1094 Ok(RestoreEntryKind::File) 1095 } 1096 Ok(_) => Ok(RestoreEntryKind::Other), 1097 Err(source) if source.kind() == std::io::ErrorKind::NotFound => { 1098 Ok(RestoreEntryKind::Missing) 1099 } 1100 Err(source) => Err(Error::RestoreFilesystem { 1101 operation: "inspect restore path", 1102 source, 1103 }), 1104 } 1105 } 1106 1107 #[cfg_attr(coverage_nightly, coverage(off))] 1108 fn remove_restore_file(path: &Path, operation: &'static str) -> Result<(), Error> { 1109 match restore_entry_kind(path)? { 1110 RestoreEntryKind::Missing => Ok(()), 1111 RestoreEntryKind::File => { 1112 fs::remove_file(path) 1113 .map_err(|source| Error::RestoreFilesystem { operation, source })?; 1114 sync_parent(path, "sync restore cleanup") 1115 } 1116 RestoreEntryKind::Other => Err(Error::RestoreRecoveryConflict(path.to_path_buf())), 1117 } 1118 } 1119 1120 #[cfg_attr(coverage_nightly, coverage(off))] 1121 fn require_sqlite_sidecars_absent(paths: &crate::Paths) -> Result<(), Error> { 1122 for live in [paths.runtime(), paths.private()] { 1123 let name = live 1124 .file_name() 1125 .and_then(|value| value.to_str()) 1126 .ok_or_else(|| Error::InvalidPath(live.to_path_buf()))?; 1127 for suffix in ["wal", "shm"] { 1128 let sidecar = live.with_file_name(format!("{name}-{suffix}")); 1129 if restore_entry_kind(&sidecar)? != RestoreEntryKind::Missing { 1130 return Err(Error::RestoreRecoveryConflict(sidecar)); 1131 } 1132 } 1133 } 1134 Ok(()) 1135 } 1136 1137 #[cfg_attr(coverage_nightly, coverage(off))] 1138 async fn copy_staged_member( 1139 source: &Path, 1140 destination: &Path, 1141 expected: &BackupMember, 1142 kind: BackupMemberKind, 1143 member_name: &'static str, 1144 runtime: bool, 1145 ) -> Result<(), Error> { 1146 let mut source_file = File::open(source).map_err(|_| Error::RestoreStagingFailed { 1147 member: member_name, 1148 })?; 1149 let mut options = fs::OpenOptions::new(); 1150 options.create_new(true).write(true); 1151 #[cfg(unix)] 1152 { 1153 use std::os::unix::fs::OpenOptionsExt; 1154 options.mode(0o600); 1155 } 1156 let mut destination_file = options.open(destination).map_err(|source| { 1157 if source.kind() == std::io::ErrorKind::AlreadyExists { 1158 Error::RestoreStagingAlreadyExists(destination.to_path_buf()) 1159 } else { 1160 Error::BackupFilesystem { 1161 operation: "create restore staging member", 1162 source, 1163 } 1164 } 1165 })?; 1166 std::io::copy(&mut source_file, &mut destination_file).map_err(|_| { 1167 Error::RestoreStagingFailed { 1168 member: member_name, 1169 } 1170 })?; 1171 destination_file 1172 .sync_all() 1173 .map_err(|source| Error::BackupFilesystem { 1174 operation: "sync restore staging member", 1175 source, 1176 })?; 1177 drop(destination_file); 1178 verify_member(destination, expected, kind, member_name, runtime).await 1179 } 1180 1181 #[cfg_attr(coverage_nightly, coverage(off))] 1182 fn sync_parent(path: &Path, operation: &'static str) -> Result<(), Error> { 1183 let parent = path 1184 .parent() 1185 .ok_or_else(|| Error::InvalidPath(path.to_path_buf()))?; 1186 sync_directory(parent, operation) 1187 } 1188 1189 #[cfg_attr(coverage_nightly, coverage(off))] 1190 fn create_private_directory(path: &Path, operation: &'static str) -> Result<(), Error> { 1191 #[cfg(unix)] 1192 let mut builder = fs::DirBuilder::new(); 1193 #[cfg(not(unix))] 1194 let builder = fs::DirBuilder::new(); 1195 #[cfg(unix)] 1196 { 1197 use std::os::unix::fs::DirBuilderExt; 1198 builder.mode(0o700); 1199 } 1200 builder 1201 .create(path) 1202 .map_err(|source| Error::BackupFilesystem { operation, source }) 1203 } 1204 1205 #[cfg_attr(coverage_nightly, coverage(off))] 1206 async fn capture_member( 1207 pool: &SqlitePool, 1208 destination: &Path, 1209 relative_path: &'static str, 1210 kind: BackupMemberKind, 1211 ) -> Result<BackupMember, Error> { 1212 let destination = destination 1213 .to_str() 1214 .ok_or_else(|| Error::InvalidBackupRoot(destination.to_path_buf()))?; 1215 let mut connection = pool 1216 .acquire() 1217 .await 1218 .map_err(|_| Error::BackupBackendUnavailable)?; 1219 sqlx::query("VACUUM INTO ?") 1220 .bind(destination) 1221 .execute(&mut *connection) 1222 .await 1223 .map_err(|_| Error::BackupCaptureFailed { 1224 member: relative_path, 1225 })?; 1226 drop(connection); 1227 member_from_file(Path::new(destination), relative_path, kind) 1228 } 1229 1230 #[cfg_attr(coverage_nightly, coverage(off))] 1231 fn member_from_file( 1232 path: &Path, 1233 relative_path: &'static str, 1234 kind: BackupMemberKind, 1235 ) -> Result<BackupMember, Error> { 1236 let mut file = File::open(path).map_err(|source| Error::BackupFilesystem { 1237 operation: "open captured member", 1238 source, 1239 })?; 1240 file.sync_all().map_err(|source| Error::BackupFilesystem { 1241 operation: "sync captured member", 1242 source, 1243 })?; 1244 let byte_length = file 1245 .metadata() 1246 .map_err(|source| Error::BackupFilesystem { 1247 operation: "inspect captured member", 1248 source, 1249 })? 1250 .len(); 1251 let mut sha256 = Sha256::new(); 1252 let mut buffer = [0_u8; 16 * 1_024]; 1253 loop { 1254 let read = file 1255 .read(&mut buffer) 1256 .map_err(|source| Error::BackupFilesystem { 1257 operation: "hash captured member", 1258 source, 1259 })?; 1260 if read == 0 { 1261 break; 1262 } 1263 sha256.update(&buffer[..read]); 1264 } 1265 BackupMember::new( 1266 relative_path, 1267 kind, 1268 byte_length, 1269 MemberDigest::new(sha256.finalize().into()), 1270 ) 1271 .map_err(|_| Error::BackupCaptureFailed { 1272 member: relative_path, 1273 }) 1274 } 1275 1276 #[cfg_attr(coverage_nightly, coverage(off))] 1277 fn sync_directory(path: &Path, operation: &'static str) -> Result<(), Error> { 1278 File::open(path) 1279 .and_then(|directory| directory.sync_all()) 1280 .map_err(|source| Error::BackupFilesystem { operation, source }) 1281 } 1282 1283 #[cfg_attr(coverage_nightly, coverage(off))] 1284 async fn verify_bundle( 1285 bundle: &Path, 1286 plan: &BackupPlan, 1287 manifest: &BackupManifest, 1288 ) -> Result<(), Error> { 1289 if entry_kind(bundle)? != EntryKind::Directory { 1290 return Err(Error::BackupBundleMissing(bundle.to_path_buf())); 1291 } 1292 validate_manifest(plan, manifest)?; 1293 let expected_root = if plan.secret_policy() == BackupSecretPolicy::IncludeProtectedStorage { 1294 BTreeSet::from(["private", "runtime"]) 1295 } else { 1296 BTreeSet::from(["runtime"]) 1297 }; 1298 validate_entries(bundle, &expected_root)?; 1299 let runtime_directory = bundle.join("runtime"); 1300 validate_entries(&runtime_directory, &BTreeSet::from([RUNTIME_DATABASE]))?; 1301 verify_member( 1302 &runtime_directory.join(RUNTIME_DATABASE), 1303 manifest 1304 .member(RUNTIME_MEMBER) 1305 .ok_or(Error::BackupVerificationFailed { 1306 member: RUNTIME_MEMBER, 1307 })?, 1308 BackupMemberKind::Runtime, 1309 RUNTIME_MEMBER, 1310 true, 1311 ) 1312 .await?; 1313 if plan.secret_policy() == BackupSecretPolicy::IncludeProtectedStorage { 1314 let private_directory = bundle.join("private"); 1315 validate_entries(&private_directory, &BTreeSet::from([PRIVATE_DATABASE]))?; 1316 verify_member( 1317 &private_directory.join(PRIVATE_DATABASE), 1318 manifest 1319 .member(PRIVATE_MEMBER) 1320 .ok_or(Error::BackupVerificationFailed { 1321 member: PRIVATE_MEMBER, 1322 })?, 1323 BackupMemberKind::Protected, 1324 PRIVATE_MEMBER, 1325 false, 1326 ) 1327 .await?; 1328 } 1329 Ok(()) 1330 } 1331 1332 fn validate_manifest(plan: &BackupPlan, manifest: &BackupManifest) -> Result<(), Error> { 1333 if plan.format_version() != BackupFormatVersion::V1 1334 || manifest.format_version() != plan.format_version() 1335 || manifest.backup_id() != plan.backup_id() 1336 || manifest.secret_policy() != plan.secret_policy() 1337 || manifest.created_at_unix_ms() != plan.requested_at_unix_ms() 1338 { 1339 return Err(Error::BackupVerificationFailed { member: "manifest" }); 1340 } 1341 let expected = if plan.secret_policy() == BackupSecretPolicy::IncludeProtectedStorage { 1342 BTreeSet::from([PRIVATE_MEMBER, RUNTIME_MEMBER]) 1343 } else { 1344 BTreeSet::from([RUNTIME_MEMBER]) 1345 }; 1346 let actual = manifest 1347 .members() 1348 .iter() 1349 .map(BackupMember::relative_path) 1350 .collect::<BTreeSet<_>>(); 1351 if actual == expected { 1352 Ok(()) 1353 } else { 1354 Err(Error::BackupVerificationFailed { member: "manifest" }) 1355 } 1356 } 1357 1358 #[cfg_attr(coverage_nightly, coverage(off))] 1359 fn validate_entries(directory: &Path, expected: &BTreeSet<&str>) -> Result<(), Error> { 1360 let mut actual = BTreeSet::new(); 1361 let entries = fs::read_dir(directory).map_err(|source| Error::BackupFilesystem { 1362 operation: "read backup bundle directory", 1363 source, 1364 })?; 1365 for entry in entries { 1366 let entry = entry.map_err(|source| Error::BackupFilesystem { 1367 operation: "read backup bundle entry", 1368 source, 1369 })?; 1370 let name = entry 1371 .file_name() 1372 .into_string() 1373 .map_err(|_| Error::BackupUnexpectedEntry(entry.path()))?; 1374 let metadata = 1375 fs::symlink_metadata(entry.path()).map_err(|source| Error::BackupFilesystem { 1376 operation: "inspect backup bundle entry", 1377 source, 1378 })?; 1379 if metadata.file_type().is_symlink() || !expected.contains(name.as_str()) { 1380 return Err(Error::BackupUnexpectedEntry(entry.path())); 1381 } 1382 actual.insert(name); 1383 } 1384 if actual.iter().map(String::as_str).collect::<BTreeSet<_>>() == *expected { 1385 Ok(()) 1386 } else { 1387 Err(Error::BackupVerificationFailed { 1388 member: "inventory", 1389 }) 1390 } 1391 } 1392 1393 #[cfg_attr(coverage_nightly, coverage(off))] 1394 async fn verify_member( 1395 path: &Path, 1396 expected: &BackupMember, 1397 expected_kind: BackupMemberKind, 1398 member_name: &'static str, 1399 runtime: bool, 1400 ) -> Result<(), Error> { 1401 if expected.kind() != expected_kind || !entry_kind_file(path)? { 1402 return Err(Error::BackupVerificationFailed { 1403 member: member_name, 1404 }); 1405 } 1406 let (byte_length, sha256) = fingerprint(path)?; 1407 if byte_length != expected.byte_length() || sha256 != expected.sha256() { 1408 return Err(Error::BackupVerificationFailed { 1409 member: member_name, 1410 }); 1411 } 1412 let mut connection = SqliteConnection::connect_with( 1413 &SqliteConnectOptions::new() 1414 .filename(path) 1415 .read_only(true) 1416 .foreign_keys(true), 1417 ) 1418 .await 1419 .map_err(|_| Error::BackupVerificationFailed { 1420 member: member_name, 1421 })?; 1422 let schema = if runtime { 1423 migration::migrate_runtime(&mut connection, OpenMode::ReadOnly).await 1424 } else { 1425 migration::migrate_private(&mut connection, OpenMode::ReadOnly).await 1426 }; 1427 if schema.is_err() 1428 || integrity::check_connection(&mut connection).await != integrity::MemberOutcome::Verified 1429 { 1430 return Err(Error::BackupVerificationFailed { 1431 member: member_name, 1432 }); 1433 } 1434 connection 1435 .close() 1436 .await 1437 .map_err(|_| Error::BackupVerificationFailed { 1438 member: member_name, 1439 }) 1440 } 1441 1442 #[cfg_attr(coverage_nightly, coverage(off))] 1443 fn entry_kind_file(path: &Path) -> Result<bool, Error> { 1444 match fs::symlink_metadata(path) { 1445 Ok(metadata) => Ok(metadata.is_file() && !metadata.file_type().is_symlink()), 1446 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(false), 1447 Err(source) => Err(Error::BackupFilesystem { 1448 operation: "inspect backup member", 1449 source, 1450 }), 1451 } 1452 } 1453 1454 #[cfg_attr(coverage_nightly, coverage(off))] 1455 fn fingerprint(path: &Path) -> Result<(u64, MemberDigest), Error> { 1456 let mut file = File::open(path).map_err(|source| Error::BackupFilesystem { 1457 operation: "open backup member for verification", 1458 source, 1459 })?; 1460 let byte_length = file 1461 .metadata() 1462 .map_err(|source| Error::BackupFilesystem { 1463 operation: "inspect backup member for verification", 1464 source, 1465 })? 1466 .len(); 1467 let mut sha256 = Sha256::new(); 1468 let mut buffer = [0_u8; 16 * 1_024]; 1469 loop { 1470 let read = file 1471 .read(&mut buffer) 1472 .map_err(|source| Error::BackupFilesystem { 1473 operation: "hash backup member for verification", 1474 source, 1475 })?; 1476 if read == 0 { 1477 break; 1478 } 1479 sha256.update(&buffer[..read]); 1480 } 1481 Ok((byte_length, MemberDigest::new(sha256.finalize().into()))) 1482 } 1483 1484 #[cfg(test)] 1485 #[cfg_attr(coverage_nightly, coverage(off))] 1486 mod tests { 1487 use radroots_storage::{ 1488 backup::{ 1489 BackupFormatVersion, BackupId, BackupMemberKind, BackupPlan, BackupSecretPolicy, 1490 MemberVerification, RestorePlan, 1491 }, 1492 event::SourceGeneration, 1493 status::ShutdownState, 1494 }; 1495 use serde::Deserialize; 1496 use sqlx::{Connection, Row, SqliteConnection, sqlite::SqliteConnectOptions}; 1497 1498 use crate::{OpenMode, OpenOptions, Paths}; 1499 1500 use super::*; 1501 1502 const POLICY: &str = include_str!("../../../contracts/storage/backup_capture_policy_v1.toml"); 1503 const FINALIZE_POLICY: &str = 1504 include_str!("../../../contracts/storage/backup_finalize_policy_v1.toml"); 1505 const RESTORE_POLICY: &str = 1506 include_str!("../../../contracts/storage/restore_staging_policy_v1.toml"); 1507 const RESTORE_FINALIZE_POLICY: &str = 1508 include_str!("../../../contracts/storage/restore_finalize_policy_v1.toml"); 1509 const FAILURE_POLICY: &str = 1510 include_str!("../../../contracts/storage/failure_injection_policy_v1.toml"); 1511 1512 #[derive(Deserialize)] 1513 struct Policy { 1514 schema_version: u32, 1515 format_version: u16, 1516 backup_root: String, 1517 staging_name: String, 1518 final_name: String, 1519 capture: String, 1520 runtime_member: String, 1521 protected_member: String, 1522 exclude_protected_members: Vec<String>, 1523 include_protected_members: Vec<String>, 1524 created_at: String, 1525 member_digest: String, 1526 member_length: String, 1527 filesystem_sync: Vec<String>, 1528 existing_staging_or_final: String, 1529 hidden_clock: bool, 1530 unsafe_ffi: bool, 1531 } 1532 1533 #[derive(Deserialize)] 1534 struct FinalizePolicy { 1535 schema_version: u32, 1536 verification: Vec<String>, 1537 finalization: String, 1538 root_sync_after_rename: bool, 1539 finalized_retry: String, 1540 missing_bundle: String, 1541 staging_and_final_present: String, 1542 unexpected_entry: String, 1543 mutation_before_complete_verification: bool, 1544 } 1545 1546 #[derive(Deserialize)] 1547 struct RestorePolicy { 1548 schema_version: u32, 1549 source: String, 1550 authority: String, 1551 runtime_staging: String, 1552 protected_staging: String, 1553 creation: String, 1554 verification: Vec<String>, 1555 live_mutation: bool, 1556 existing_staging: String, 1557 protected_member: String, 1558 filesystem_sync: Vec<String>, 1559 } 1560 1561 #[derive(Deserialize)] 1562 struct RestoreFinalizePolicy { 1563 schema_version: u32, 1564 authority: String, 1565 quiescence: String, 1566 wal_sidecars: String, 1567 marker: String, 1568 marker_encoding: String, 1569 marker_durability: Vec<String>, 1570 previous_runtime: String, 1571 previous_protected: String, 1572 replacement: Vec<String>, 1573 recovery: String, 1574 read_only_recovery: String, 1575 cleanup_order: Vec<String>, 1576 backend_after_attempt: String, 1577 } 1578 1579 #[derive(Deserialize)] 1580 struct FailurePolicy { 1581 schema_version: u32, 1582 strategy: String, 1583 runtime_global_hooks: bool, 1584 accepted_reopen_outcomes: Vec<String>, 1585 atomic_sql_points: Vec<String>, 1586 migration_points: Vec<String>, 1587 backup_points: Vec<String>, 1588 restore_points: Vec<String>, 1589 lock_close_points: Vec<String>, 1590 restore_recovery: String, 1591 failure_reporting: String, 1592 } 1593 1594 #[derive(Clone, Copy, Debug)] 1595 #[repr(u8)] 1596 enum RestoreCrashPoint { 1597 MarkerPersisted = 0, 1598 RuntimePreviousRenamed = 1, 1599 RuntimeReplacementRenamed = 2, 1600 ProtectedPreviousRenamed = 3, 1601 ProtectedReplacementRenamed = 4, 1602 RuntimePreviousCleaned = 5, 1603 ProtectedPreviousCleaned = 6, 1604 MarkerCleaned = 7, 1605 } 1606 1607 impl RestoreCrashPoint { 1608 const ALL: [Self; 8] = [ 1609 Self::MarkerPersisted, 1610 Self::RuntimePreviousRenamed, 1611 Self::RuntimeReplacementRenamed, 1612 Self::ProtectedPreviousRenamed, 1613 Self::ProtectedReplacementRenamed, 1614 Self::RuntimePreviousCleaned, 1615 Self::ProtectedPreviousCleaned, 1616 Self::MarkerCleaned, 1617 ]; 1618 1619 const fn reached(self, point: Self) -> bool { 1620 self as u8 >= point as u8 1621 } 1622 } 1623 1624 fn generation(byte: u8) -> SourceGeneration { 1625 SourceGeneration::new([byte; 32]).expect("source generation") 1626 } 1627 1628 fn plan(byte: u8, policy: BackupSecretPolicy, at: u64) -> BackupPlan { 1629 BackupPlan::new( 1630 BackupId::new([byte; 16]).expect("backup id"), 1631 BackupFormatVersion::V1, 1632 policy, 1633 at, 1634 ) 1635 .expect("backup plan") 1636 } 1637 1638 async fn create(database_root: &Path, backup_root: Option<&Path>) -> (Paths, SqliteStorage) { 1639 let paths = Paths::from_directory(database_root).expect("owned paths"); 1640 let mut options = OpenOptions::new(paths.clone(), OpenMode::Create) 1641 .with_source_generation(generation(91), 9_100) 1642 .expect("source generation"); 1643 if let Some(root) = backup_root { 1644 options = options.with_backup_root(root).expect("backup root"); 1645 } 1646 let store = SqliteStorage::open(options).await.expect("create storage"); 1647 (paths, store) 1648 } 1649 1650 async fn scalar(path: &Path, query: &'static str) -> i64 { 1651 let mut connection = SqliteConnection::connect_with( 1652 &SqliteConnectOptions::new().filename(path).read_only(true), 1653 ) 1654 .await 1655 .expect("open captured member"); 1656 let value = sqlx::query(query) 1657 .fetch_one(&mut connection) 1658 .await 1659 .expect("query captured member") 1660 .try_get(0) 1661 .expect("decode captured member"); 1662 connection.close().await.expect("close captured member"); 1663 value 1664 } 1665 1666 #[tokio::test] 1667 async fn aggregate_reliability_state_is_idempotent_conflict_safe_and_close_aware() { 1668 let database_root = tempfile::tempdir().expect("database root"); 1669 let (_paths, store) = create(database_root.path(), None).await; 1670 let backup = plan(44, BackupSecretPolicy::ExcludeProtectedStorage, 4_400); 1671 1672 let planned = StorageReliability::begin_backup(&store, backup.clone()) 1673 .await 1674 .expect("planned backup"); 1675 assert_eq!( 1676 StorageReliability::begin_backup(&store, backup.clone()) 1677 .await 1678 .expect("idempotent backup"), 1679 planned 1680 ); 1681 let conflicting = plan(44, BackupSecretPolicy::IncludeProtectedStorage, 4_400); 1682 assert_eq!( 1683 StorageReliability::begin_backup(&store, conflicting).await, 1684 Err(StorageError::ReliabilityRevisionConflict) 1685 ); 1686 1687 let manifest = BackupManifest::new( 1688 backup.format_version(), 1689 backup.backup_id(), 1690 backup.requested_at_unix_ms(), 1691 backup.secret_policy(), 1692 vec![ 1693 BackupMember::new( 1694 RUNTIME_MEMBER, 1695 BackupMemberKind::Runtime, 1696 1, 1697 MemberDigest::new([1; 32]), 1698 ) 1699 .expect("runtime member"), 1700 ], 1701 ) 1702 .expect("restore manifest"); 1703 let restore = RestorePlan::new( 1704 manifest.clone(), 1705 BackupSecretPolicy::ExcludeProtectedStorage, 1706 4_401, 1707 ) 1708 .expect("restore plan"); 1709 let staging = StorageReliability::begin_restore(&store, restore.clone()) 1710 .await 1711 .expect("staging restore"); 1712 assert_eq!( 1713 StorageReliability::begin_restore(&store, restore) 1714 .await 1715 .expect("idempotent restore"), 1716 staging 1717 ); 1718 let conflicting_restore = 1719 RestorePlan::new(manifest, BackupSecretPolicy::ExcludeProtectedStorage, 4_402) 1720 .expect("conflicting restore plan"); 1721 assert_eq!( 1722 StorageReliability::begin_restore(&store, conflicting_restore).await, 1723 Err(StorageError::ReliabilityRevisionConflict) 1724 ); 1725 1726 let failed = StorageReliability::transition_backup( 1727 &store, 1728 backup.backup_id(), 1729 planned.revision(), 1730 BackupTransition::Fail, 1731 4_401, 1732 ) 1733 .await 1734 .expect("failed transition"); 1735 assert_eq!( 1736 failed.stage(), 1737 radroots_storage::backup::BackupStage::Failed 1738 ); 1739 assert_eq!( 1740 StorageReliability::transition_backup( 1741 &store, 1742 backup.backup_id(), 1743 failed.revision(), 1744 BackupTransition::Fail, 1745 4_402, 1746 ) 1747 .await, 1748 Err(StorageError::ReliabilityOperationTerminal) 1749 ); 1750 1751 let status = StorageReliability::status(&store) 1752 .await 1753 .expect("open status"); 1754 assert_eq!(status.shutdown(), ShutdownState::Open); 1755 let closed = StorageReliability::close(&store) 1756 .await 1757 .expect("close storage"); 1758 assert_eq!(closed.shutdown(), ShutdownState::Closed); 1759 assert_eq!( 1760 StorageReliability::begin_backup( 1761 &store, 1762 plan(45, BackupSecretPolicy::ExcludeProtectedStorage, 4_500) 1763 ) 1764 .await, 1765 Err(StorageError::BackendUnavailable) 1766 ); 1767 } 1768 1769 async fn insert_private_artifact(store: &SqliteStorage, byte: u8) { 1770 sqlx::query( 1771 "INSERT INTO radroots_private_artifacts ( 1772 artifact_id, artifact_kind, schema_id, commitment, 1773 protected_size_bytes, secret_provider, secret_reference, 1774 key_version, envelope_version, encrypted_envelope, 1775 delete_not_before_unix_ms, expires_at_unix_ms, revision, stage, 1776 created_at_unix_ms, updated_at_unix_ms, deleted_at_unix_ms, 1777 deletion_reason, tombstone_commitment 1778 ) VALUES (?, 'test', 'test.v1', ?, 1, 'test', 'ref', 1, 1779 NULL, NULL, NULL, NULL, 1, 'active', 1, 1, NULL, NULL, NULL)", 1780 ) 1781 .bind(vec![byte; 16]) 1782 .bind(vec![byte; 32]) 1783 .execute(&store.private_pool) 1784 .await 1785 .expect("insert private artifact"); 1786 } 1787 1788 fn construct_restore_crash_state( 1789 layout: &RestoreLayout, 1790 marker: &RestoreMarker, 1791 point: RestoreCrashPoint, 1792 ) { 1793 write_restore_marker(&layout.marker, marker).expect("persist interruption marker"); 1794 if point.reached(RestoreCrashPoint::RuntimePreviousRenamed) { 1795 fs::rename(&layout.runtime_live, &layout.runtime_previous) 1796 .expect("rename runtime previous"); 1797 sync_parent(&layout.runtime_live, "sync runtime previous") 1798 .expect("sync runtime previous"); 1799 } 1800 if point.reached(RestoreCrashPoint::RuntimeReplacementRenamed) { 1801 fs::rename(&layout.runtime_staging, &layout.runtime_live) 1802 .expect("promote runtime replacement"); 1803 sync_parent(&layout.runtime_live, "sync runtime replacement") 1804 .expect("sync runtime replacement"); 1805 } 1806 if point.reached(RestoreCrashPoint::ProtectedPreviousRenamed) { 1807 fs::rename(&layout.private_live, &layout.private_previous) 1808 .expect("rename protected previous"); 1809 sync_parent(&layout.private_live, "sync protected previous") 1810 .expect("sync protected previous"); 1811 } 1812 if point.reached(RestoreCrashPoint::ProtectedReplacementRenamed) { 1813 fs::rename(&layout.private_staging, &layout.private_live) 1814 .expect("promote protected replacement"); 1815 sync_parent(&layout.private_live, "sync protected replacement") 1816 .expect("sync protected replacement"); 1817 } 1818 if point.reached(RestoreCrashPoint::RuntimePreviousCleaned) { 1819 remove_restore_file(&layout.runtime_previous, "inject runtime cleanup") 1820 .expect("clean runtime previous"); 1821 } 1822 if point.reached(RestoreCrashPoint::ProtectedPreviousCleaned) { 1823 remove_restore_file(&layout.private_previous, "inject protected cleanup") 1824 .expect("clean protected previous"); 1825 } 1826 if point.reached(RestoreCrashPoint::MarkerCleaned) { 1827 remove_restore_file(&layout.marker, "inject marker cleanup").expect("clean marker"); 1828 } 1829 } 1830 1831 #[test] 1832 fn implementation_matches_the_governed_backup_capture_policy() { 1833 let policy = toml::from_str::<Policy>(POLICY).expect("backup capture policy"); 1834 assert_eq!(policy.schema_version, 1); 1835 assert_eq!(policy.format_version, 1); 1836 assert_eq!( 1837 policy.backup_root, 1838 "explicit_existing_host_owned_absolute_utf8_directory" 1839 ); 1840 assert_eq!( 1841 policy.staging_name, 1842 ".radroots-backup-{backup_id_hex}.staging" 1843 ); 1844 assert_eq!(policy.final_name, "radroots-backup-{backup_id_hex}"); 1845 assert_eq!(policy.capture, "sqlite_vacuum_into"); 1846 assert_eq!(policy.runtime_member, RUNTIME_MEMBER); 1847 assert_eq!(policy.protected_member, PRIVATE_MEMBER); 1848 assert_eq!(policy.exclude_protected_members, [RUNTIME_MEMBER]); 1849 assert_eq!( 1850 policy.include_protected_members, 1851 [RUNTIME_MEMBER, PRIVATE_MEMBER] 1852 ); 1853 assert_eq!(policy.created_at, "plan_requested_at_unix_ms"); 1854 assert_eq!(policy.member_digest, "sha256"); 1855 assert_eq!(policy.member_length, "exact_bytes"); 1856 assert_eq!( 1857 policy.filesystem_sync, 1858 [ 1859 "member_file", 1860 "member_directory", 1861 "staging_directory", 1862 "backup_root" 1863 ] 1864 ); 1865 assert_eq!(policy.existing_staging_or_final, "reject"); 1866 assert!(!policy.hidden_clock); 1867 assert!(!policy.unsafe_ffi); 1868 } 1869 1870 #[test] 1871 fn implementation_matches_the_governed_backup_finalize_policy() { 1872 let policy = toml::from_str::<FinalizePolicy>(FINALIZE_POLICY).expect("finalize policy"); 1873 assert_eq!(policy.schema_version, 1); 1874 assert_eq!( 1875 policy.verification, 1876 [ 1877 "exact_plan_manifest", 1878 "exact_inventory", 1879 "no_symlinks", 1880 "exact_length", 1881 "sha256", 1882 "current_schema_catalog", 1883 "sqlite_integrity_check", 1884 "foreign_key_check" 1885 ] 1886 ); 1887 assert_eq!(policy.finalization, "same_root_atomic_directory_rename"); 1888 assert!(policy.root_sync_after_rename); 1889 assert_eq!(policy.finalized_retry, "verify_and_succeed"); 1890 assert_eq!(policy.missing_bundle, "reject"); 1891 assert_eq!(policy.staging_and_final_present, "reject"); 1892 assert_eq!(policy.unexpected_entry, "reject"); 1893 assert!(!policy.mutation_before_complete_verification); 1894 } 1895 1896 #[test] 1897 fn implementation_matches_the_governed_restore_staging_policy() { 1898 let policy = toml::from_str::<RestorePolicy>(RESTORE_POLICY).expect("restore policy"); 1899 assert_eq!(policy.schema_version, 1); 1900 assert_eq!(policy.source, "verified_finalized_backup_bundle"); 1901 assert_eq!(policy.authority, "writable_storage_only"); 1902 assert_eq!( 1903 policy.runtime_staging, 1904 ".runtime.sqlite.restore-{backup_id_hex}.staging" 1905 ); 1906 assert_eq!( 1907 policy.protected_staging, 1908 ".private.sqlite.restore-{backup_id_hex}.staging" 1909 ); 1910 assert_eq!(policy.creation, "create_new_mode_0600"); 1911 assert_eq!( 1912 policy.verification, 1913 [ 1914 "exact_length", 1915 "sha256", 1916 "current_schema_catalog", 1917 "sqlite_integrity_check", 1918 "foreign_key_check" 1919 ] 1920 ); 1921 assert!(!policy.live_mutation); 1922 assert_eq!(policy.existing_staging, "reject"); 1923 assert_eq!(policy.protected_member, "manifest_policy_controlled"); 1924 assert_eq!( 1925 policy.filesystem_sync, 1926 ["staged_member_file", "destination_parent"] 1927 ); 1928 } 1929 1930 #[test] 1931 fn implementation_matches_the_governed_restore_finalize_policy() { 1932 let policy = toml::from_str::<RestoreFinalizePolicy>(RESTORE_FINALIZE_POLICY) 1933 .expect("restore finalize policy"); 1934 assert_eq!(policy.schema_version, 1); 1935 assert_eq!( 1936 policy.authority, 1937 "open_writable_backend_with_exclusive_writer_lock" 1938 ); 1939 assert_eq!( 1940 policy.quiescence, 1941 "close_all_owned_pools_before_live_rename" 1942 ); 1943 assert_eq!( 1944 policy.wal_sidecars, 1945 "marker_precedes_and_fences_absence_before_live_rename" 1946 ); 1947 assert_eq!( 1948 policy.marker, 1949 ".radroots-storage-restore-{backup_id_hex}.marker" 1950 ); 1951 assert_eq!( 1952 policy.marker_encoding, 1953 "fixed_binary_v1_exact_member_lengths_and_sha256" 1954 ); 1955 assert_eq!( 1956 policy.marker_durability, 1957 ["marker_file_fsync", "runtime_parent_fsync"] 1958 ); 1959 assert_eq!( 1960 policy.previous_runtime, 1961 ".runtime.sqlite.restore-{backup_id_hex}.previous" 1962 ); 1963 assert_eq!( 1964 policy.previous_protected, 1965 ".private.sqlite.restore-{backup_id_hex}.previous" 1966 ); 1967 assert_eq!( 1968 policy.replacement, 1969 [ 1970 "live_to_previous_atomic_rename", 1971 "staging_to_live_atomic_rename" 1972 ] 1973 ); 1974 assert_eq!( 1975 policy.recovery, 1976 "marker_driven_idempotent_forward_completion_before_open" 1977 ); 1978 assert_eq!(policy.read_only_recovery, "reject"); 1979 assert_eq!( 1980 policy.cleanup_order, 1981 [ 1982 "verify_all_live_members", 1983 "remove_previous_members", 1984 "remove_marker" 1985 ] 1986 ); 1987 assert_eq!(policy.backend_after_attempt, "closed_reopen_required"); 1988 } 1989 1990 #[test] 1991 fn implementation_matches_the_governed_failure_injection_policy() { 1992 let policy = toml::from_str::<FailurePolicy>(FAILURE_POLICY).expect("failure policy"); 1993 assert_eq!(policy.schema_version, 1); 1994 assert_eq!( 1995 policy.strategy, 1996 "deterministic_state_construction_and_sql_faults" 1997 ); 1998 assert!(!policy.runtime_global_hooks); 1999 assert_eq!( 2000 policy.accepted_reopen_outcomes, 2001 [ 2002 "fully_committed_replayable", 2003 "typed_recoverable_no_partial_success" 2004 ] 2005 ); 2006 assert_eq!( 2007 policy.atomic_sql_points, 2008 [ 2009 "source_sequence", 2010 "event", 2011 "provenance", 2012 "projection_checkpoint", 2013 "commit_receipt", 2014 "journal", 2015 "outbox_item", 2016 "outbox_target", 2017 "delivery_evidence" 2018 ] 2019 ); 2020 assert_eq!( 2021 policy.migration_points, 2022 [ 2023 "application_identity", 2024 "each_pending_step", 2025 "user_version", 2026 "exact_catalog", 2027 "transaction_commit" 2028 ] 2029 ); 2030 assert_eq!( 2031 policy.backup_points, 2032 [ 2033 "runtime_snapshot", 2034 "protected_snapshot", 2035 "member_hash", 2036 "manifest", 2037 "complete_verification", 2038 "final_directory_rename", 2039 "root_sync" 2040 ] 2041 ); 2042 assert_eq!( 2043 policy.restore_points, 2044 [ 2045 "runtime_staging", 2046 "protected_staging", 2047 "staged_validation", 2048 "marker", 2049 "runtime_previous_rename", 2050 "runtime_replacement_rename", 2051 "protected_previous_rename", 2052 "protected_replacement_rename", 2053 "installed_validation", 2054 "runtime_previous_cleanup", 2055 "protected_previous_cleanup", 2056 "marker_cleanup" 2057 ] 2058 ); 2059 assert_eq!( 2060 policy.lock_close_points, 2061 [ 2062 "lock_file_open", 2063 "exclusive_acquisition", 2064 "cross_process_contention", 2065 "pool_drain", 2066 "lock_release", 2067 "closed_status" 2068 ] 2069 ); 2070 assert_eq!( 2071 policy.restore_recovery, 2072 "idempotent_forward_completion_before_connection_open" 2073 ); 2074 assert_eq!( 2075 policy.failure_reporting, 2076 "stable_typed_error_without_backend_details" 2077 ); 2078 } 2079 2080 #[tokio::test] 2081 async fn capture_excludes_protected_storage_and_includes_latest_wal_state() { 2082 let database_root = tempfile::tempdir().expect("database root"); 2083 let backup_parent = tempfile::tempdir().expect("backup parent"); 2084 let backup_root = backup_parent.path().join("backups"); 2085 fs::create_dir(&backup_root).expect("backup root"); 2086 let (_, store) = create(database_root.path(), Some(&backup_root)).await; 2087 sqlx::raw_sql( 2088 "CREATE TABLE runtime_backup_probe (value INTEGER NOT NULL); 2089 INSERT INTO runtime_backup_probe (value) VALUES (41);", 2090 ) 2091 .execute(&store.pool) 2092 .await 2093 .expect("runtime WAL mutation"); 2094 sqlx::raw_sql( 2095 "CREATE TABLE private_backup_probe (value INTEGER NOT NULL); 2096 INSERT INTO private_backup_probe (value) VALUES (42);", 2097 ) 2098 .execute(&store.private_pool) 2099 .await 2100 .expect("private WAL mutation"); 2101 2102 let plan = plan(92, BackupSecretPolicy::ExcludeProtectedStorage, 9_200); 2103 let manifest = store.capture_backup(&plan).await.expect("capture backup"); 2104 let layout = BackupLayout::new(&backup_root, &plan); 2105 assert_eq!(manifest.created_at_unix_ms(), 9_200); 2106 assert_eq!( 2107 manifest.secret_policy(), 2108 BackupSecretPolicy::ExcludeProtectedStorage 2109 ); 2110 assert_eq!(manifest.members().len(), 1); 2111 let runtime = &manifest.members()[0]; 2112 assert_eq!(runtime.relative_path(), RUNTIME_MEMBER); 2113 assert_eq!(runtime.kind(), BackupMemberKind::Runtime); 2114 assert_eq!( 2115 runtime.byte_length(), 2116 fs::metadata(&layout.runtime_file) 2117 .expect("runtime metadata") 2118 .len() 2119 ); 2120 assert_eq!( 2121 runtime.sha256(), 2122 member_from_file( 2123 &layout.runtime_file, 2124 RUNTIME_MEMBER, 2125 BackupMemberKind::Runtime 2126 ) 2127 .expect("rehash runtime member") 2128 .sha256() 2129 ); 2130 assert_eq!( 2131 scalar( 2132 &layout.runtime_file, 2133 "SELECT value FROM runtime_backup_probe" 2134 ) 2135 .await, 2136 41 2137 ); 2138 assert!(!layout.private_directory.exists()); 2139 assert!(!layout.finalized.exists()); 2140 assert!(matches!( 2141 store.capture_backup(&plan).await, 2142 Err(Error::BackupBundleAlreadyExists(path)) if path == layout.staging 2143 )); 2144 } 2145 2146 #[tokio::test] 2147 async fn read_only_capture_includes_protected_member_and_rejects_invalid_lifecycle() { 2148 let database_root = tempfile::tempdir().expect("database root"); 2149 let backup_parent = tempfile::tempdir().expect("backup parent"); 2150 let backup_root = backup_parent.path().join("backups"); 2151 fs::create_dir(&backup_root).expect("backup root"); 2152 let (paths, writer) = create(database_root.path(), Some(&backup_root)).await; 2153 writer.close().await.expect("close writer"); 2154 2155 let reader = SqliteStorage::open( 2156 OpenOptions::new(paths, OpenMode::ReadOnly) 2157 .with_backup_root(&backup_root) 2158 .expect("backup root"), 2159 ) 2160 .await 2161 .expect("read-only storage"); 2162 let include_plan = plan(93, BackupSecretPolicy::IncludeProtectedStorage, 9_300); 2163 let manifest = reader 2164 .capture_backup(&include_plan) 2165 .await 2166 .expect("read-only capture"); 2167 let layout = BackupLayout::new(&backup_root, &include_plan); 2168 assert_eq!(manifest.members().len(), 2); 2169 assert_eq!(manifest.members()[1].relative_path(), PRIVATE_MEMBER); 2170 assert_eq!(manifest.members()[1].kind(), BackupMemberKind::Protected); 2171 assert_eq!( 2172 scalar( 2173 &layout.private_file, 2174 "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'radroots_private_artifacts'" 2175 ) 2176 .await, 2177 1 2178 ); 2179 2180 let unsupported = BackupPlan::new( 2181 BackupId::new([94; 16]).expect("backup id"), 2182 BackupFormatVersion::new(2).expect("version"), 2183 BackupSecretPolicy::ExcludeProtectedStorage, 2184 9_400, 2185 ) 2186 .expect("unsupported plan"); 2187 assert!(matches!( 2188 reader.capture_backup(&unsupported).await, 2189 Err(Error::UnsupportedBackupVersion) 2190 )); 2191 reader.close().await.expect("close reader"); 2192 assert!(matches!( 2193 reader 2194 .capture_backup(&plan( 2195 95, 2196 BackupSecretPolicy::ExcludeProtectedStorage, 2197 9_500 2198 )) 2199 .await, 2200 Err(Error::BackupBackendUnavailable) 2201 )); 2202 } 2203 2204 #[tokio::test] 2205 async fn backup_root_is_explicit_and_fail_closed() { 2206 let database_root = tempfile::tempdir().expect("database root"); 2207 let (_, store) = create(database_root.path(), None).await; 2208 assert!(matches!( 2209 store 2210 .capture_backup(&plan( 2211 96, 2212 BackupSecretPolicy::ExcludeProtectedStorage, 2213 9_600 2214 )) 2215 .await, 2216 Err(Error::BackupRootRequired) 2217 )); 2218 2219 let relative = PathBuf::from("backups"); 2220 assert!(matches!( 2221 OpenOptions::new( 2222 Paths::from_directory(database_root.path()).expect("owned paths"), 2223 OpenMode::ReadOnly 2224 ) 2225 .with_backup_root(relative), 2226 Err(Error::InvalidBackupRoot(_)) 2227 )); 2228 let file = database_root.path().join("backup-file"); 2229 fs::write(&file, b"not a directory").expect("backup file"); 2230 assert!(matches!( 2231 validate_backup_root(&file), 2232 Err(Error::InvalidBackupRoot(_)) 2233 )); 2234 2235 #[cfg(unix)] 2236 { 2237 use std::os::unix::fs::symlink; 2238 let directory = database_root.path().join("real-backups"); 2239 let alias = database_root.path().join("backup-alias"); 2240 fs::create_dir(&directory).expect("real backup root"); 2241 symlink(&directory, &alias).expect("backup root symlink"); 2242 assert!(matches!( 2243 validate_backup_root(&alias), 2244 Err(Error::InvalidBackupRoot(_)) 2245 )); 2246 } 2247 } 2248 2249 #[tokio::test] 2250 async fn complete_bundle_verifies_finalizes_atomically_and_retries_idempotently() { 2251 let database_root = tempfile::tempdir().expect("database root"); 2252 let backup_parent = tempfile::tempdir().expect("backup parent"); 2253 let backup_root = backup_parent.path().join("backups"); 2254 fs::create_dir(&backup_root).expect("backup root"); 2255 let (_, store) = create(database_root.path(), Some(&backup_root)).await; 2256 let plan = plan(97, BackupSecretPolicy::IncludeProtectedStorage, 9_700); 2257 let manifest = store.capture_backup(&plan).await.expect("capture backup"); 2258 let layout = BackupLayout::new(&backup_root, &plan); 2259 2260 store 2261 .verify_backup(&plan, &manifest) 2262 .await 2263 .expect("verify staging bundle"); 2264 let finalized = store 2265 .finalize_backup(&plan, &manifest) 2266 .await 2267 .expect("finalize backup"); 2268 assert_eq!(finalized, layout.finalized); 2269 assert!(!layout.staging.exists()); 2270 assert!(layout.finalized.is_dir()); 2271 assert_eq!( 2272 store 2273 .finalize_backup(&plan, &manifest) 2274 .await 2275 .expect("idempotent finalization"), 2276 layout.finalized 2277 ); 2278 } 2279 2280 #[tokio::test] 2281 async fn verification_rejects_tampering_unexpected_entries_and_missing_bundles() { 2282 let database_root = tempfile::tempdir().expect("database root"); 2283 let backup_parent = tempfile::tempdir().expect("backup parent"); 2284 let backup_root = backup_parent.path().join("backups"); 2285 fs::create_dir(&backup_root).expect("backup root"); 2286 let (_, store) = create(database_root.path(), Some(&backup_root)).await; 2287 2288 let tampered_plan = plan(98, BackupSecretPolicy::ExcludeProtectedStorage, 9_800); 2289 let tampered_manifest = store 2290 .capture_backup(&tampered_plan) 2291 .await 2292 .expect("capture tamper target"); 2293 let tampered_layout = BackupLayout::new(&backup_root, &tampered_plan); 2294 use std::io::Write; 2295 fs::OpenOptions::new() 2296 .append(true) 2297 .open(&tampered_layout.runtime_file) 2298 .expect("open tamper target") 2299 .write_all(b"tamper") 2300 .expect("tamper member"); 2301 assert!(matches!( 2302 store 2303 .verify_backup(&tampered_plan, &tampered_manifest) 2304 .await, 2305 Err(Error::BackupVerificationFailed { 2306 member: RUNTIME_MEMBER 2307 }) 2308 )); 2309 assert!(!tampered_layout.finalized.exists()); 2310 2311 let unexpected_plan = plan(99, BackupSecretPolicy::ExcludeProtectedStorage, 9_900); 2312 let unexpected_manifest = store 2313 .capture_backup(&unexpected_plan) 2314 .await 2315 .expect("capture unexpected target"); 2316 let unexpected_layout = BackupLayout::new(&backup_root, &unexpected_plan); 2317 fs::write(unexpected_layout.staging.join("unexpected"), b"data").expect("unexpected entry"); 2318 assert!(matches!( 2319 store 2320 .verify_backup(&unexpected_plan, &unexpected_manifest) 2321 .await, 2322 Err(Error::BackupUnexpectedEntry(_)) 2323 )); 2324 2325 #[cfg(unix)] 2326 { 2327 use std::os::unix::fs::symlink; 2328 fs::remove_file(unexpected_layout.staging.join("unexpected")) 2329 .expect("remove unexpected entry"); 2330 fs::remove_file(&unexpected_layout.runtime_file).expect("remove captured member"); 2331 symlink( 2332 database_root.path().join(RUNTIME_DATABASE), 2333 &unexpected_layout.runtime_file, 2334 ) 2335 .expect("symlink captured member"); 2336 assert!(matches!( 2337 store 2338 .verify_backup(&unexpected_plan, &unexpected_manifest) 2339 .await, 2340 Err(Error::BackupUnexpectedEntry(_)) 2341 )); 2342 } 2343 2344 let missing_plan = plan(100, BackupSecretPolicy::ExcludeProtectedStorage, 10_000); 2345 let missing_manifest = BackupManifest::new( 2346 missing_plan.format_version(), 2347 missing_plan.backup_id(), 2348 missing_plan.requested_at_unix_ms(), 2349 missing_plan.secret_policy(), 2350 vec![ 2351 BackupMember::new( 2352 RUNTIME_MEMBER, 2353 BackupMemberKind::Runtime, 2354 1, 2355 MemberDigest::new([1; 32]), 2356 ) 2357 .expect("member"), 2358 ], 2359 ) 2360 .expect("manifest"); 2361 assert!(matches!( 2362 store 2363 .finalize_backup(&missing_plan, &missing_manifest) 2364 .await, 2365 Err(Error::BackupBundleMissing(_)) 2366 )); 2367 } 2368 2369 #[tokio::test] 2370 async fn restore_staging_is_verified_isolated_and_leaves_live_state_untouched() { 2371 let database_root = tempfile::tempdir().expect("database root"); 2372 let backup_parent = tempfile::tempdir().expect("backup parent"); 2373 let backup_root = backup_parent.path().join("backups"); 2374 fs::create_dir(&backup_root).expect("backup root"); 2375 let (paths, store) = create(database_root.path(), Some(&backup_root)).await; 2376 sqlx::query( 2377 "UPDATE radroots_runtime_source_generations SET sequence_head = 51 WHERE state = 'active'", 2378 ) 2379 .execute(&store.pool) 2380 .await 2381 .expect("initial live state"); 2382 let backup_plan = plan(101, BackupSecretPolicy::IncludeProtectedStorage, 10_100); 2383 let manifest = store 2384 .capture_backup(&backup_plan) 2385 .await 2386 .expect("capture restore source"); 2387 store 2388 .finalize_backup(&backup_plan, &manifest) 2389 .await 2390 .expect("finalize restore source"); 2391 sqlx::query( 2392 "UPDATE radroots_runtime_source_generations SET sequence_head = 52 WHERE state = 'active'", 2393 ) 2394 .execute(&store.pool) 2395 .await 2396 .expect("advance live state"); 2397 2398 let restore = RestorePlan::new( 2399 manifest.clone(), 2400 BackupSecretPolicy::IncludeProtectedStorage, 2401 10_200, 2402 ) 2403 .expect("restore plan"); 2404 let statuses = store.stage_restore(&restore).await.expect("stage restore"); 2405 assert_eq!(statuses.len(), 2); 2406 assert!( 2407 statuses 2408 .iter() 2409 .all(|status| status.verification() == MemberVerification::Verified) 2410 ); 2411 let staging = RestoreStaging::new(&paths, &manifest).expect("restore staging paths"); 2412 assert_eq!( 2413 scalar( 2414 paths.runtime(), 2415 "SELECT sequence_head FROM radroots_runtime_source_generations WHERE state = 'active'" 2416 ) 2417 .await, 2418 52 2419 ); 2420 assert_eq!( 2421 scalar( 2422 &staging.runtime, 2423 "SELECT sequence_head FROM radroots_runtime_source_generations WHERE state = 'active'" 2424 ) 2425 .await, 2426 51 2427 ); 2428 assert!(staging.private.is_file()); 2429 assert!(matches!( 2430 store.stage_restore(&restore).await, 2431 Err(Error::RestoreStagingAlreadyExists(_)) 2432 )); 2433 2434 #[cfg(unix)] 2435 { 2436 use std::os::unix::fs::PermissionsExt; 2437 assert_eq!( 2438 fs::metadata(&staging.runtime) 2439 .expect("staged runtime metadata") 2440 .permissions() 2441 .mode() 2442 & 0o777, 2443 0o600 2444 ); 2445 } 2446 2447 store.close().await.expect("close writer"); 2448 let reader = SqliteStorage::open( 2449 OpenOptions::new(paths, OpenMode::ReadOnly) 2450 .with_backup_root(&backup_root) 2451 .expect("backup root"), 2452 ) 2453 .await 2454 .expect("read-only store"); 2455 assert!(matches!( 2456 reader.stage_restore(&restore).await, 2457 Err(Error::RestoreRequiresWritableStorage) 2458 )); 2459 } 2460 2461 #[tokio::test] 2462 async fn restore_finalization_atomically_replaces_every_selected_member_and_closes() { 2463 let database_root = tempfile::tempdir().expect("database root"); 2464 let backup_parent = tempfile::tempdir().expect("backup parent"); 2465 let backup_root = backup_parent.path().join("backups"); 2466 fs::create_dir(&backup_root).expect("backup root"); 2467 let (paths, store) = create(database_root.path(), Some(&backup_root)).await; 2468 sqlx::query( 2469 "UPDATE radroots_runtime_source_generations SET sequence_head = 61 WHERE state = 'active'", 2470 ) 2471 .execute(&store.pool) 2472 .await 2473 .expect("initial runtime state"); 2474 insert_private_artifact(&store, 1).await; 2475 let backup_plan = plan(102, BackupSecretPolicy::IncludeProtectedStorage, 10_300); 2476 let manifest = store 2477 .capture_backup(&backup_plan) 2478 .await 2479 .expect("capture restore source"); 2480 store 2481 .finalize_backup(&backup_plan, &manifest) 2482 .await 2483 .expect("finalize restore source"); 2484 sqlx::query( 2485 "UPDATE radroots_runtime_source_generations SET sequence_head = 62 WHERE state = 'active'", 2486 ) 2487 .execute(&store.pool) 2488 .await 2489 .expect("advance runtime state"); 2490 insert_private_artifact(&store, 2).await; 2491 let restore = RestorePlan::new( 2492 manifest.clone(), 2493 BackupSecretPolicy::IncludeProtectedStorage, 2494 10_400, 2495 ) 2496 .expect("restore plan"); 2497 store.stage_restore(&restore).await.expect("stage restore"); 2498 let layout = RestoreLayout::new(&paths, manifest.backup_id()).expect("restore layout"); 2499 let held_connection = store.pool.acquire().await.expect("held connection"); 2500 let restoring = store.clone(); 2501 let restore_plan = restore.clone(); 2502 let finalization = tokio::spawn(async move { 2503 restoring 2504 .finalize_restore(&restore_plan) 2505 .await 2506 .expect("finalize restore") 2507 }); 2508 tokio::time::timeout(std::time::Duration::from_secs(30), async { 2509 loop { 2510 if store 2511 .storage_status() 2512 .await 2513 .expect("restoring status") 2514 .shutdown() 2515 == ShutdownState::Closing 2516 { 2517 break; 2518 } 2519 tokio::task::yield_now().await; 2520 } 2521 }) 2522 .await 2523 .expect("restore enters closing state"); 2524 assert!(!finalization.is_finished()); 2525 assert_eq!( 2526 store 2527 .storage_status() 2528 .await 2529 .expect("restoring status") 2530 .shutdown(), 2531 ShutdownState::Closing 2532 ); 2533 let concurrent_close_store = store.clone(); 2534 let concurrent_close = tokio::spawn(async move { 2535 concurrent_close_store 2536 .close() 2537 .await 2538 .expect("concurrent close") 2539 }); 2540 tokio::task::yield_now().await; 2541 assert!(!concurrent_close.is_finished()); 2542 assert!(matches!( 2543 SqliteStorage::open(OpenOptions::new(paths.clone(), OpenMode::ReadWriteExisting)).await, 2544 Err(Error::WriterAlreadyActive { .. }) 2545 )); 2546 drop(held_connection); 2547 let (finalization, concurrent_close) = tokio::join!(finalization, concurrent_close); 2548 finalization.expect("restore finalization task"); 2549 let close_status = concurrent_close.expect("concurrent close task"); 2550 assert!(matches!( 2551 close_status.shutdown(), 2552 ShutdownState::Closing | ShutdownState::Closed 2553 )); 2554 assert_eq!( 2555 store 2556 .storage_status() 2557 .await 2558 .expect("closed restore status") 2559 .shutdown(), 2560 ShutdownState::Closed 2561 ); 2562 2563 let reopened = SqliteStorage::open( 2564 OpenOptions::new(paths.clone(), OpenMode::ReadWriteExisting) 2565 .with_backup_root(&backup_root) 2566 .expect("backup root"), 2567 ) 2568 .await 2569 .expect("reopen restored storage"); 2570 assert_eq!( 2571 scalar( 2572 paths.runtime(), 2573 "SELECT sequence_head FROM radroots_runtime_source_generations WHERE state = 'active'" 2574 ) 2575 .await, 2576 61 2577 ); 2578 assert_eq!( 2579 scalar( 2580 paths.private(), 2581 "SELECT COUNT(*) FROM radroots_private_artifacts" 2582 ) 2583 .await, 2584 1 2585 ); 2586 for path in [ 2587 layout.runtime_staging, 2588 layout.private_staging, 2589 layout.runtime_previous, 2590 layout.private_previous, 2591 layout.marker, 2592 ] { 2593 assert!( 2594 !path.exists(), 2595 "restore artifact remained: {}", 2596 path.display() 2597 ); 2598 } 2599 reopened.close().await.expect("close restored storage"); 2600 } 2601 2602 #[tokio::test] 2603 async fn writable_open_completes_an_interrupted_restore_before_connections_open() { 2604 let database_root = tempfile::tempdir().expect("database root"); 2605 let backup_parent = tempfile::tempdir().expect("backup parent"); 2606 let backup_root = backup_parent.path().join("backups"); 2607 fs::create_dir(&backup_root).expect("backup root"); 2608 let (paths, store) = create(database_root.path(), Some(&backup_root)).await; 2609 sqlx::query( 2610 "UPDATE radroots_runtime_source_generations SET sequence_head = 71 WHERE state = 'active'", 2611 ) 2612 .execute(&store.pool) 2613 .await 2614 .expect("initial runtime state"); 2615 insert_private_artifact(&store, 3).await; 2616 let backup_plan = plan(103, BackupSecretPolicy::IncludeProtectedStorage, 10_500); 2617 let manifest = store 2618 .capture_backup(&backup_plan) 2619 .await 2620 .expect("capture restore source"); 2621 store 2622 .finalize_backup(&backup_plan, &manifest) 2623 .await 2624 .expect("finalize restore source"); 2625 sqlx::query( 2626 "UPDATE radroots_runtime_source_generations SET sequence_head = 72 WHERE state = 'active'", 2627 ) 2628 .execute(&store.pool) 2629 .await 2630 .expect("advance runtime state"); 2631 insert_private_artifact(&store, 4).await; 2632 let restore = RestorePlan::new( 2633 manifest.clone(), 2634 BackupSecretPolicy::IncludeProtectedStorage, 2635 10_600, 2636 ) 2637 .expect("restore plan"); 2638 store.stage_restore(&restore).await.expect("stage restore"); 2639 store.close().await.expect("close before simulated crash"); 2640 require_sqlite_sidecars_absent(&paths).expect("quiesced SQLite sidecars"); 2641 let marker = RestoreMarker::from_manifest(&manifest).expect("restore marker"); 2642 let layout = RestoreLayout::new(&paths, manifest.backup_id()).expect("restore layout"); 2643 write_restore_marker(&layout.marker, &marker).expect("persist restore marker"); 2644 fs::rename(&layout.runtime_live, &layout.runtime_previous) 2645 .expect("simulate interrupted previous rename"); 2646 fs::rename(&layout.runtime_staging, &layout.runtime_live) 2647 .expect("simulate installed runtime member"); 2648 sync_parent(&layout.runtime_live, "sync simulated interruption") 2649 .expect("sync simulated interruption"); 2650 2651 assert!(matches!( 2652 SqliteStorage::open(OpenOptions::new(paths.clone(), OpenMode::ReadOnly)).await, 2653 Err(Error::RestoreRequiresWritableStorage) 2654 )); 2655 let runtime_wal = paths.runtime().with_file_name("runtime.sqlite-wal"); 2656 fs::write(&runtime_wal, b"simulated reader sidecar").expect("simulated WAL sidecar"); 2657 assert!(matches!( 2658 SqliteStorage::open(OpenOptions::new( 2659 paths.clone(), 2660 OpenMode::ReadWriteExisting 2661 )) 2662 .await, 2663 Err(Error::RestoreRecoveryConflict(path)) if path == runtime_wal 2664 )); 2665 assert!(layout.marker.is_file()); 2666 fs::remove_file(&runtime_wal).expect("remove simulated WAL sidecar"); 2667 sync_parent(&runtime_wal, "sync simulated WAL cleanup") 2668 .expect("sync simulated WAL cleanup"); 2669 let recovered = 2670 SqliteStorage::open(OpenOptions::new(paths.clone(), OpenMode::ReadWriteExisting)) 2671 .await 2672 .expect("recover interrupted restore"); 2673 assert_eq!( 2674 scalar( 2675 paths.runtime(), 2676 "SELECT sequence_head FROM radroots_runtime_source_generations WHERE state = 'active'" 2677 ) 2678 .await, 2679 71 2680 ); 2681 assert_eq!( 2682 scalar( 2683 paths.private(), 2684 "SELECT COUNT(*) FROM radroots_private_artifacts" 2685 ) 2686 .await, 2687 1 2688 ); 2689 for path in [ 2690 layout.runtime_staging, 2691 layout.private_staging, 2692 layout.runtime_previous, 2693 layout.private_previous, 2694 layout.marker, 2695 ] { 2696 assert!( 2697 !path.exists(), 2698 "recovery artifact remained: {}", 2699 path.display() 2700 ); 2701 } 2702 recovered.close().await.expect("close recovered storage"); 2703 } 2704 2705 #[tokio::test] 2706 async fn every_durable_restore_crash_point_recovers_to_one_complete_installation() { 2707 for (index, point) in RestoreCrashPoint::ALL.into_iter().enumerate() { 2708 let database_root = tempfile::tempdir().expect("database root"); 2709 let backup_parent = tempfile::tempdir().expect("backup parent"); 2710 let backup_root = backup_parent.path().join("backups"); 2711 fs::create_dir(&backup_root).expect("backup root"); 2712 let (paths, store) = create(database_root.path(), Some(&backup_root)).await; 2713 sqlx::query( 2714 "UPDATE radroots_runtime_source_generations SET sequence_head = 81 WHERE state = 'active'", 2715 ) 2716 .execute(&store.pool) 2717 .await 2718 .expect("backup runtime state"); 2719 insert_private_artifact(&store, 5).await; 2720 let backup_plan = plan( 2721 110 + u8::try_from(index).expect("crash index"), 2722 BackupSecretPolicy::IncludeProtectedStorage, 2723 11_000 + u64::try_from(index).expect("crash index"), 2724 ); 2725 let manifest = store 2726 .capture_backup(&backup_plan) 2727 .await 2728 .expect("capture crash source"); 2729 store 2730 .finalize_backup(&backup_plan, &manifest) 2731 .await 2732 .expect("finalize crash source"); 2733 sqlx::query( 2734 "UPDATE radroots_runtime_source_generations SET sequence_head = 82 WHERE state = 'active'", 2735 ) 2736 .execute(&store.pool) 2737 .await 2738 .expect("advance runtime state"); 2739 insert_private_artifact(&store, 6).await; 2740 let restore = RestorePlan::new( 2741 manifest.clone(), 2742 BackupSecretPolicy::IncludeProtectedStorage, 2743 12_000 + u64::try_from(index).expect("crash index"), 2744 ) 2745 .expect("restore plan"); 2746 store.stage_restore(&restore).await.expect("stage restore"); 2747 store.close().await.expect("quiesce crash state"); 2748 require_sqlite_sidecars_absent(&paths).expect("quiesced SQLite sidecars"); 2749 let marker = RestoreMarker::from_manifest(&manifest).expect("restore marker"); 2750 let layout = RestoreLayout::new(&paths, manifest.backup_id()).expect("restore layout"); 2751 construct_restore_crash_state(&layout, &marker, point); 2752 2753 let recovered = 2754 SqliteStorage::open(OpenOptions::new(paths.clone(), OpenMode::ReadWriteExisting)) 2755 .await 2756 .unwrap_or_else(|error| panic!("recover {point:?}: {error}")); 2757 assert_eq!( 2758 scalar( 2759 paths.runtime(), 2760 "SELECT sequence_head FROM radroots_runtime_source_generations WHERE state = 'active'" 2761 ) 2762 .await, 2763 81, 2764 "runtime state after {point:?}" 2765 ); 2766 assert_eq!( 2767 scalar( 2768 paths.private(), 2769 "SELECT COUNT(*) FROM radroots_private_artifacts" 2770 ) 2771 .await, 2772 1, 2773 "protected state after {point:?}" 2774 ); 2775 for artifact in [ 2776 &layout.runtime_staging, 2777 &layout.private_staging, 2778 &layout.runtime_previous, 2779 &layout.private_previous, 2780 &layout.marker, 2781 ] { 2782 assert!( 2783 !artifact.exists(), 2784 "artifact after {point:?}: {}", 2785 artifact.display() 2786 ); 2787 } 2788 recovered.close().await.expect("close recovered state"); 2789 } 2790 } 2791 2792 #[tokio::test] 2793 async fn corrupt_restore_markers_fail_closed_without_opening_live_state() { 2794 let database_root = tempfile::tempdir().expect("database root"); 2795 let (paths, store) = create(database_root.path(), None).await; 2796 store.close().await.expect("close storage"); 2797 let backup_id = BackupId::new([104; 16]).expect("backup id"); 2798 let layout = RestoreLayout::new(&paths, backup_id).expect("restore layout"); 2799 fs::write(&layout.marker, [0_u8; RESTORE_MARKER_BYTES]).expect("corrupt marker"); 2800 sync_parent(&layout.marker, "sync corrupt marker").expect("sync corrupt marker"); 2801 assert!(matches!( 2802 SqliteStorage::open(OpenOptions::new(paths, OpenMode::ReadWriteExisting)).await, 2803 Err(Error::RestoreMarkerCorrupt(_)) 2804 )); 2805 2806 let marker_path = Path::new("restore.marker"); 2807 for private in [ 2808 None, 2809 Some(RestoreMemberExpectation { 2810 byte_length: 2, 2811 sha256: MemberDigest::new([2; 32]), 2812 }), 2813 ] { 2814 let marker = RestoreMarker { 2815 backup_id, 2816 secret_policy: if private.is_some() { 2817 BackupSecretPolicy::IncludeProtectedStorage 2818 } else { 2819 BackupSecretPolicy::ExcludeProtectedStorage 2820 }, 2821 runtime: RestoreMemberExpectation { 2822 byte_length: 1, 2823 sha256: MemberDigest::new([1; 32]), 2824 }, 2825 private, 2826 }; 2827 let encoded = marker.encode(); 2828 assert_eq!( 2829 RestoreMarker::decode(marker_path, &encoded) 2830 .expect("decode marker") 2831 .encode(), 2832 encoded 2833 ); 2834 for end in 0..encoded.len() { 2835 let _ = RestoreMarker::decode(marker_path, &encoded[..end]); 2836 } 2837 for index in 0..encoded.len() { 2838 let mut corrupt = encoded; 2839 corrupt[index] ^= 0xff; 2840 let _ = RestoreMarker::decode(marker_path, &corrupt); 2841 } 2842 } 2843 2844 let valid = RestoreMarker { 2845 backup_id, 2846 secret_policy: BackupSecretPolicy::ExcludeProtectedStorage, 2847 runtime: RestoreMemberExpectation { 2848 byte_length: 1, 2849 sha256: MemberDigest::new([1; 32]), 2850 }, 2851 private: None, 2852 } 2853 .encode(); 2854 let mut zero_runtime = valid; 2855 zero_runtime[25..33].copy_from_slice(&0_u64.to_be_bytes()); 2856 assert!(RestoreMarker::decode(marker_path, &zero_runtime).is_err()); 2857 let mut unexpected_private = valid; 2858 unexpected_private[65..73].copy_from_slice(&1_u64.to_be_bytes()); 2859 assert!(RestoreMarker::decode(marker_path, &unexpected_private).is_err()); 2860 } 2861 2862 #[test] 2863 fn manifest_validation_rejects_each_governed_identity_mismatch() { 2864 fn manifest( 2865 id: u8, 2866 policy: BackupSecretPolicy, 2867 created_at: u64, 2868 runtime_path: &'static str, 2869 ) -> BackupManifest { 2870 let mut members = vec![ 2871 BackupMember::new( 2872 runtime_path, 2873 BackupMemberKind::Runtime, 2874 1, 2875 MemberDigest::new([1; 32]), 2876 ) 2877 .expect("runtime member"), 2878 ]; 2879 if policy == BackupSecretPolicy::IncludeProtectedStorage { 2880 members.push( 2881 BackupMember::new( 2882 PRIVATE_MEMBER, 2883 BackupMemberKind::Protected, 2884 2, 2885 MemberDigest::new([2; 32]), 2886 ) 2887 .expect("private member"), 2888 ); 2889 } 2890 BackupManifest::new( 2891 BackupFormatVersion::V1, 2892 BackupId::new([id; 16]).expect("backup id"), 2893 created_at, 2894 policy, 2895 members, 2896 ) 2897 .expect("backup manifest") 2898 } 2899 2900 let plan = plan(120, BackupSecretPolicy::ExcludeProtectedStorage, 12_000); 2901 let valid = manifest( 2902 120, 2903 BackupSecretPolicy::ExcludeProtectedStorage, 2904 12_000, 2905 RUNTIME_MEMBER, 2906 ); 2907 assert!(validate_manifest(&plan, &valid).is_ok()); 2908 for invalid in [ 2909 manifest( 2910 121, 2911 BackupSecretPolicy::ExcludeProtectedStorage, 2912 12_000, 2913 RUNTIME_MEMBER, 2914 ), 2915 manifest( 2916 120, 2917 BackupSecretPolicy::IncludeProtectedStorage, 2918 12_000, 2919 RUNTIME_MEMBER, 2920 ), 2921 manifest( 2922 120, 2923 BackupSecretPolicy::ExcludeProtectedStorage, 2924 12_001, 2925 RUNTIME_MEMBER, 2926 ), 2927 manifest( 2928 120, 2929 BackupSecretPolicy::ExcludeProtectedStorage, 2930 12_000, 2931 "runtime/alternate.sqlite", 2932 ), 2933 ] { 2934 assert!(matches!( 2935 validate_manifest(&plan, &invalid), 2936 Err(Error::BackupVerificationFailed { member: "manifest" }) 2937 )); 2938 } 2939 } 2940 }