lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }