lib

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

marker.rs (97077B)


      1 //! Sealed v1 restore-recovery marker and durable descriptor-relative store.
      2 
      3 #![allow(dead_code)] // Step 066 freezes private primitives consumed by Steps 067-069.
      4 
      5 use core::{fmt, num::NonZeroU32};
      6 use std::{error::Error, path::PathBuf};
      7 
      8 #[cfg(test)]
      9 use std::path::Path;
     10 
     11 use radroots_runtime_paths::{InstanceId, ServiceId};
     12 use radroots_storage::event::SourceGeneration;
     13 use serde::{Deserialize, Serialize};
     14 use sha2::{Digest, Sha256};
     15 
     16 use crate::{
     17     BackupManifestSha256, ExistingServiceDatabaseIntent, ServiceDatabaseIdentity,
     18     ServiceDatabaseMetadata, ServiceSqliteApplicationId, ServiceSqlitePaths,
     19 };
     20 
     21 #[cfg(any(target_os = "linux", target_os = "macos"))]
     22 use crate::{ServiceSqliteError, ServiceSqliteErrorKind, WriterAuthority};
     23 
     24 const RESTORE_MARKER_SCHEMA: &str = "radroots.service-sqlite.restore-marker";
     25 const RESTORE_MARKER_SCHEMA_VERSION: u32 = 1;
     26 const RESTORE_MARKER_MAX_BYTES: usize = 2_048;
     27 const RESTORE_MARKER_CHECKSUM_DOMAIN: &[u8] = b"radroots.service_sqlite.restore_marker.v1\0";
     28 pub(crate) const LIVE_FILE_NAME: &str = radroots_runtime_paths::SERVICE_STATE_DATABASE_FILE_NAME;
     29 pub(crate) const STAGED_FILE_NAME: &str = "state.restore-staged.sqlite";
     30 pub(crate) const BACKUP_FILE_NAME: &str = "state.restore-backup.sqlite";
     31 pub(crate) const MARKER_FILE_NAME: &str = "state.restore-marker.v1";
     32 pub(crate) const MARKER_NEXT_FILE_NAME: &str = "state.restore-marker.v1.next";
     33 
     34 /// Exact retained identity and content expected for one restore artifact.
     35 #[derive(Clone, Copy, PartialEq, Eq)]
     36 pub(crate) struct RestoreArtifactExpectation {
     37     device: u64,
     38     inode: u64,
     39     byte_length: u64,
     40     sha256: [u8; 32],
     41 }
     42 
     43 impl RestoreArtifactExpectation {
     44     pub(crate) const fn new(
     45         device: u64,
     46         inode: u64,
     47         byte_length: u64,
     48         sha256: [u8; 32],
     49     ) -> Result<Self, RestoreMarkerContractError> {
     50         if byte_length == 0 || byte_length > i64::MAX as u64 {
     51             return Err(RestoreMarkerContractError::InvalidIdentity);
     52         }
     53         Ok(Self {
     54             device,
     55             inode,
     56             byte_length,
     57             sha256,
     58         })
     59     }
     60 
     61     pub(crate) const fn device(self) -> u64 {
     62         self.device
     63     }
     64 
     65     pub(crate) const fn inode(self) -> u64 {
     66         self.inode
     67     }
     68 
     69     pub(crate) const fn byte_length(self) -> u64 {
     70         self.byte_length
     71     }
     72 
     73     pub(crate) const fn sha256(self) -> [u8; 32] {
     74         self.sha256
     75     }
     76 }
     77 
     78 impl fmt::Debug for RestoreArtifactExpectation {
     79     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     80         formatter.write_str("RestoreArtifactExpectation([redacted])")
     81     }
     82 }
     83 
     84 /// Durable phases in the v1 restore-replacement protocol.
     85 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     86 pub(crate) enum RestoreRecoveryPhase {
     87     Prepared,
     88     LiveRetained,
     89     ReplacementInstalled,
     90 }
     91 
     92 impl RestoreRecoveryPhase {
     93     const fn as_str(self) -> &'static str {
     94         match self {
     95             Self::Prepared => "prepared",
     96             Self::LiveRetained => "live_retained",
     97             Self::ReplacementInstalled => "replacement_installed",
     98         }
     99     }
    100 
    101     fn parse(value: &str) -> Result<Self, RestoreMarkerContractError> {
    102         match value {
    103             "prepared" => Ok(Self::Prepared),
    104             "live_retained" => Ok(Self::LiveRetained),
    105             "replacement_installed" => Ok(Self::ReplacementInstalled),
    106             _ => Err(RestoreMarkerContractError::UnsupportedValue),
    107         }
    108     }
    109 
    110     const fn may_transition_to(self, next: Self) -> bool {
    111         self as u8 == next as u8
    112             || matches!(
    113                 (self, next),
    114                 (Self::Prepared, Self::LiveRetained)
    115                     | (Self::LiveRetained, Self::ReplacementInstalled)
    116             )
    117     }
    118 }
    119 
    120 /// Fixed recovery-artifact layout adjacent to canonical service state.
    121 #[derive(Clone, PartialEq, Eq)]
    122 pub(crate) struct RestoreRecoveryLayout {
    123     state_directory: PathBuf,
    124     live: PathBuf,
    125     staged: PathBuf,
    126     backup: PathBuf,
    127     marker: PathBuf,
    128     marker_next: PathBuf,
    129 }
    130 
    131 impl RestoreRecoveryLayout {
    132     pub(crate) fn for_paths(
    133         paths: &ServiceSqlitePaths,
    134     ) -> Result<Self, RestoreMarkerContractError> {
    135         let live = paths.state_database();
    136         let state_directory = live
    137             .parent()
    138             .filter(|parent| Some(*parent) == paths.state_lock().parent())
    139             .filter(|_| live.file_name().is_some_and(|name| name == LIVE_FILE_NAME))
    140             .filter(|parent| parent.is_absolute())
    141             .ok_or(RestoreMarkerContractError::InvalidLayout)?;
    142         Ok(Self {
    143             state_directory: state_directory.to_path_buf(),
    144             live: state_directory.join(LIVE_FILE_NAME),
    145             staged: state_directory.join(STAGED_FILE_NAME),
    146             backup: state_directory.join(BACKUP_FILE_NAME),
    147             marker: state_directory.join(MARKER_FILE_NAME),
    148             marker_next: state_directory.join(MARKER_NEXT_FILE_NAME),
    149         })
    150     }
    151 
    152     pub(crate) fn state_directory(&self) -> &PathBuf {
    153         &self.state_directory
    154     }
    155 
    156     pub(crate) fn staged(&self) -> &PathBuf {
    157         &self.staged
    158     }
    159 
    160     #[cfg(test)]
    161     fn file_names(&self) -> [&str; 5] {
    162         [
    163             LIVE_FILE_NAME,
    164             STAGED_FILE_NAME,
    165             BACKUP_FILE_NAME,
    166             MARKER_FILE_NAME,
    167             MARKER_NEXT_FILE_NAME,
    168         ]
    169     }
    170 }
    171 
    172 impl fmt::Debug for RestoreRecoveryLayout {
    173     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    174         formatter.write_str("RestoreRecoveryLayout([redacted])")
    175     }
    176 }
    177 
    178 /// Immutable canonical v1 restore-recovery marker.
    179 #[derive(Clone, PartialEq, Eq)]
    180 pub(crate) struct RestoreRecoveryMarker {
    181     phase: RestoreRecoveryPhase,
    182     service: ServiceId,
    183     instance: InstanceId,
    184     source_generation: SourceGeneration,
    185     state_schema_version: NonZeroU32,
    186     application_id: ServiceSqliteApplicationId,
    187     source_manifest_sha256: BackupManifestSha256,
    188     live: RestoreArtifactExpectation,
    189     staged: RestoreArtifactExpectation,
    190     backup: RestoreArtifactExpectation,
    191     canonical_bytes: Box<[u8]>,
    192 }
    193 
    194 impl RestoreRecoveryMarker {
    195     #[allow(clippy::too_many_arguments)]
    196     pub(crate) fn prepared(
    197         metadata: &ServiceDatabaseMetadata,
    198         source_manifest_sha256: BackupManifestSha256,
    199         live: RestoreArtifactExpectation,
    200         staged: RestoreArtifactExpectation,
    201     ) -> Result<Self, RestoreMarkerContractError> {
    202         Self::build(
    203             RestoreRecoveryPhase::Prepared,
    204             metadata.service().clone(),
    205             metadata.instance().clone(),
    206             metadata.source_generation(),
    207             metadata.state_schema_version(),
    208             metadata.application_id(),
    209             source_manifest_sha256,
    210             live,
    211             staged,
    212             live,
    213         )
    214     }
    215 
    216     pub(crate) fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, RestoreMarkerContractError> {
    217         require_marker_contract(
    218             !bytes.is_empty(),
    219             RestoreMarkerContractError::MalformedEncoding,
    220         )?;
    221         require_marker_contract(
    222             bytes.len() <= RESTORE_MARKER_MAX_BYTES,
    223             RestoreMarkerContractError::MarkerTooLarge,
    224         )?;
    225         let wire: WireMarker = serde_json::from_slice(bytes)
    226             .map_err(|_| RestoreMarkerContractError::MalformedEncoding)?;
    227         require_marker_contract(
    228             wire.schema == RESTORE_MARKER_SCHEMA,
    229             RestoreMarkerContractError::UnsupportedValue,
    230         )?;
    231         require_marker_contract(
    232             wire.schema_version == RESTORE_MARKER_SCHEMA_VERSION,
    233             RestoreMarkerContractError::UnsupportedValue,
    234         )?;
    235         let marker = Self::build(
    236             RestoreRecoveryPhase::parse(&wire.phase)?,
    237             ServiceId::new(wire.service)
    238                 .map_err(|_| RestoreMarkerContractError::InvalidIdentity)?,
    239             InstanceId::new(wire.instance)
    240                 .map_err(|_| RestoreMarkerContractError::InvalidIdentity)?,
    241             SourceGeneration::new(decode_hex_32(&wire.source_generation)?)
    242                 .map_err(|_| RestoreMarkerContractError::InvalidIdentity)?,
    243             NonZeroU32::new(wire.state_schema_version)
    244                 .ok_or(RestoreMarkerContractError::InvalidIdentity)?,
    245             ServiceSqliteApplicationId::new(wire.application_id)
    246                 .map_err(|_| RestoreMarkerContractError::InvalidIdentity)?,
    247             BackupManifestSha256::from_bytes(decode_hex_32(&wire.source_manifest_sha256)?),
    248             RestoreArtifactExpectation::try_from(wire.live)?,
    249             RestoreArtifactExpectation::try_from(wire.staged)?,
    250             RestoreArtifactExpectation::try_from(wire.backup)?,
    251         )?;
    252         let claimed = decode_hex_32(&wire.marker_sha256)?;
    253         let actual = marker_checksum(&marker.payload_bytes()?);
    254         require_marker_contract(
    255             claimed == actual,
    256             RestoreMarkerContractError::ChecksumMismatch,
    257         )?;
    258         require_marker_contract(
    259             marker.canonical_bytes.as_ref() == bytes,
    260             RestoreMarkerContractError::NonCanonicalEncoding,
    261         )?;
    262         Ok(marker)
    263     }
    264 
    265     pub(crate) fn canonical_bytes(&self) -> &[u8] {
    266         &self.canonical_bytes
    267     }
    268 
    269     pub(crate) const fn phase(&self) -> RestoreRecoveryPhase {
    270         self.phase
    271     }
    272 
    273     pub(crate) const fn live(&self) -> RestoreArtifactExpectation {
    274         self.live
    275     }
    276 
    277     pub(crate) const fn staged(&self) -> RestoreArtifactExpectation {
    278         self.staged
    279     }
    280 
    281     pub(crate) const fn backup(&self) -> RestoreArtifactExpectation {
    282         self.backup
    283     }
    284 
    285     pub(crate) fn transitioned_to(
    286         &self,
    287         next: RestoreRecoveryPhase,
    288     ) -> Result<Self, RestoreMarkerContractError> {
    289         require_marker_contract(
    290             self.phase.may_transition_to(next),
    291             RestoreMarkerContractError::IllegalTransition,
    292         )?;
    293         if self.phase == next {
    294             return Ok(self.clone());
    295         }
    296         Self::build(
    297             next,
    298             self.service.clone(),
    299             self.instance.clone(),
    300             self.source_generation,
    301             self.state_schema_version,
    302             self.application_id,
    303             self.source_manifest_sha256,
    304             self.live,
    305             self.staged,
    306             self.backup,
    307         )
    308     }
    309 
    310     #[allow(clippy::too_many_arguments)]
    311     fn build(
    312         phase: RestoreRecoveryPhase,
    313         service: ServiceId,
    314         instance: InstanceId,
    315         source_generation: SourceGeneration,
    316         state_schema_version: NonZeroU32,
    317         application_id: ServiceSqliteApplicationId,
    318         source_manifest_sha256: BackupManifestSha256,
    319         live: RestoreArtifactExpectation,
    320         staged: RestoreArtifactExpectation,
    321         backup: RestoreArtifactExpectation,
    322     ) -> Result<Self, RestoreMarkerContractError> {
    323         require_marker_contract(
    324             crate::all_constraints([
    325                 live == backup,
    326                 (live.device, live.inode) != (staged.device, staged.inode),
    327             ]),
    328             RestoreMarkerContractError::InvalidIdentity,
    329         )?;
    330         let mut marker = Self {
    331             phase,
    332             service,
    333             instance,
    334             source_generation,
    335             state_schema_version,
    336             application_id,
    337             source_manifest_sha256,
    338             live,
    339             staged,
    340             backup,
    341             canonical_bytes: Box::new([]),
    342         };
    343         let payload = marker.payload_bytes()?;
    344         let checksum = encode_hex(&marker_checksum(&payload));
    345         let canonical = marker.wire(&checksum);
    346         let canonical_bytes = serde_json::to_vec(&canonical)
    347             .map_err(|_| RestoreMarkerContractError::EncodingFailure)?;
    348         require_marker_contract(
    349             canonical_bytes.len() <= RESTORE_MARKER_MAX_BYTES,
    350             RestoreMarkerContractError::MarkerTooLarge,
    351         )?;
    352         marker.canonical_bytes = canonical_bytes.into_boxed_slice();
    353         Ok(marker)
    354     }
    355 
    356     fn payload_bytes(&self) -> Result<Vec<u8>, RestoreMarkerContractError> {
    357         serde_json::to_vec(&self.payload()).map_err(|_| RestoreMarkerContractError::EncodingFailure)
    358     }
    359 
    360     fn payload(&self) -> CanonicalPayload<'_> {
    361         CanonicalPayload {
    362             schema: RESTORE_MARKER_SCHEMA,
    363             schema_version: RESTORE_MARKER_SCHEMA_VERSION,
    364             phase: self.phase.as_str(),
    365             service: self.service.as_str(),
    366             instance: self.instance.as_str(),
    367             source_generation: encode_hex(self.source_generation.as_bytes()),
    368             state_schema_version: self.state_schema_version.get(),
    369             application_id: self.application_id.get(),
    370             source_manifest_sha256: encode_hex(self.source_manifest_sha256.as_bytes()),
    371             live: self.live.into(),
    372             staged: self.staged.into(),
    373             backup: self.backup.into(),
    374         }
    375     }
    376 
    377     fn wire<'a>(&'a self, marker_sha256: &'a str) -> CanonicalMarker<'a> {
    378         CanonicalMarker {
    379             payload: self.payload(),
    380             marker_sha256,
    381         }
    382     }
    383 
    384     fn matches_paths(&self, paths: &ServiceSqlitePaths) -> bool {
    385         crate::all_constraints([
    386             self.service == *paths.service(),
    387             self.instance == *paths.instance(),
    388         ])
    389     }
    390 
    391     pub(crate) fn matches_identity(&self, identity: &ServiceDatabaseIdentity) -> bool {
    392         crate::all_constraints([
    393             self.service == *identity.service(),
    394             self.instance == *identity.instance(),
    395             self.source_generation == identity.source_generation(),
    396             self.application_id == identity.application_id(),
    397             self.state_schema_version <= identity.supported_state_schema_version(),
    398         ])
    399     }
    400 
    401     pub(crate) fn matches_existing_intent(&self, intent: &ExistingServiceDatabaseIntent) -> bool {
    402         crate::all_constraints([
    403             self.service == *intent.service(),
    404             self.instance == *intent.instance(),
    405             self.application_id == intent.application_id(),
    406             self.state_schema_version <= intent.supported_state_schema_version(),
    407         ])
    408     }
    409 }
    410 
    411 impl fmt::Debug for RestoreRecoveryMarker {
    412     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    413         formatter
    414             .debug_struct("RestoreRecoveryMarker")
    415             .field("schema_version", &RESTORE_MARKER_SCHEMA_VERSION)
    416             .field("phase", &self.phase)
    417             .finish_non_exhaustive()
    418     }
    419 }
    420 
    421 /// Source-free validation failure for private restore markers.
    422 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    423 pub(crate) enum RestoreMarkerContractError {
    424     MarkerTooLarge,
    425     MalformedEncoding,
    426     NonCanonicalEncoding,
    427     EncodingFailure,
    428     UnsupportedValue,
    429     ChecksumMismatch,
    430     InvalidIdentity,
    431     InvalidLayout,
    432     IllegalTransition,
    433 }
    434 
    435 fn require_marker_contract(
    436     condition: bool,
    437     error: RestoreMarkerContractError,
    438 ) -> Result<(), RestoreMarkerContractError> {
    439     if condition { Ok(()) } else { Err(error) }
    440 }
    441 
    442 fn layout_uses_fixed_marker_name(layout: &RestoreRecoveryLayout) -> bool {
    443     layout
    444         .marker
    445         .file_name()
    446         .is_some_and(|name| name == MARKER_FILE_NAME)
    447 }
    448 
    449 fn interrupted_successor_matches(
    450     current: &RestoreRecoveryMarker,
    451     scratch: &RestoreRecoveryMarker,
    452 ) -> bool {
    453     scratch.phase() != current.phase()
    454         && current
    455             .transitioned_to(scratch.phase())
    456             .is_ok_and(|expected| expected.canonical_bytes() == scratch.canonical_bytes())
    457 }
    458 
    459 impl fmt::Display for RestoreMarkerContractError {
    460     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    461         formatter.write_str(match self {
    462             Self::MarkerTooLarge => "restore marker exceeds its byte limit",
    463             Self::MalformedEncoding => "restore marker encoding is malformed",
    464             Self::NonCanonicalEncoding => "restore marker encoding is not canonical",
    465             Self::EncodingFailure => "restore marker could not be encoded",
    466             Self::UnsupportedValue => "restore marker schema or value is unsupported",
    467             Self::ChecksumMismatch => "restore marker checksum does not match",
    468             Self::InvalidIdentity => "restore marker identity is invalid",
    469             Self::InvalidLayout => "restore recovery layout is invalid",
    470             Self::IllegalTransition => "restore marker transition is illegal",
    471         })
    472     }
    473 }
    474 
    475 impl Error for RestoreMarkerContractError {}
    476 
    477 #[derive(Serialize)]
    478 struct CanonicalPayload<'a> {
    479     schema: &'static str,
    480     schema_version: u32,
    481     phase: &'static str,
    482     service: &'a str,
    483     instance: &'a str,
    484     source_generation: String,
    485     state_schema_version: u32,
    486     application_id: u32,
    487     source_manifest_sha256: String,
    488     live: CanonicalArtifact,
    489     staged: CanonicalArtifact,
    490     backup: CanonicalArtifact,
    491 }
    492 
    493 #[derive(Serialize)]
    494 struct CanonicalMarker<'a> {
    495     #[serde(flatten)]
    496     payload: CanonicalPayload<'a>,
    497     marker_sha256: &'a str,
    498 }
    499 
    500 #[derive(Clone, Serialize)]
    501 struct CanonicalArtifact {
    502     device: u64,
    503     inode: u64,
    504     byte_length: u64,
    505     sha256: String,
    506 }
    507 
    508 impl From<RestoreArtifactExpectation> for CanonicalArtifact {
    509     fn from(value: RestoreArtifactExpectation) -> Self {
    510         Self {
    511             device: value.device,
    512             inode: value.inode,
    513             byte_length: value.byte_length,
    514             sha256: encode_hex(&value.sha256),
    515         }
    516     }
    517 }
    518 
    519 #[derive(Deserialize)]
    520 #[serde(deny_unknown_fields)]
    521 struct WireMarker {
    522     schema: String,
    523     schema_version: u32,
    524     phase: String,
    525     service: String,
    526     instance: String,
    527     source_generation: String,
    528     state_schema_version: u32,
    529     application_id: u32,
    530     source_manifest_sha256: String,
    531     live: WireArtifact,
    532     staged: WireArtifact,
    533     backup: WireArtifact,
    534     marker_sha256: String,
    535 }
    536 
    537 #[derive(Deserialize)]
    538 #[serde(deny_unknown_fields)]
    539 struct WireArtifact {
    540     device: u64,
    541     inode: u64,
    542     byte_length: u64,
    543     sha256: String,
    544 }
    545 
    546 impl TryFrom<WireArtifact> for RestoreArtifactExpectation {
    547     type Error = RestoreMarkerContractError;
    548 
    549     fn try_from(value: WireArtifact) -> Result<Self, Self::Error> {
    550         Self::new(
    551             value.device,
    552             value.inode,
    553             value.byte_length,
    554             decode_hex_32(&value.sha256)?,
    555         )
    556     }
    557 }
    558 
    559 fn marker_checksum(payload: &[u8]) -> [u8; 32] {
    560     let mut hasher = Sha256::new();
    561     hasher.update(RESTORE_MARKER_CHECKSUM_DOMAIN);
    562     hasher.update(
    563         u64::try_from(payload.len())
    564             .expect("bounded marker payload")
    565             .to_be_bytes(),
    566     );
    567     hasher.update(payload);
    568     hasher.finalize().into()
    569 }
    570 
    571 fn encode_hex(bytes: &[u8; 32]) -> String {
    572     const HEX: &[u8; 16] = b"0123456789abcdef";
    573     let mut output = String::with_capacity(64);
    574     for byte in bytes {
    575         output.push(char::from(HEX[usize::from(byte >> 4)]));
    576         output.push(char::from(HEX[usize::from(byte & 0x0f)]));
    577     }
    578     output
    579 }
    580 
    581 fn decode_hex_32(value: &str) -> Result<[u8; 32], RestoreMarkerContractError> {
    582     if value.len() != 64 {
    583         return Err(RestoreMarkerContractError::InvalidIdentity);
    584     }
    585     let mut output = [0_u8; 32];
    586     for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
    587         output[index] = (decode_nibble(pair[0])? << 4) | decode_nibble(pair[1])?;
    588     }
    589     Ok(output)
    590 }
    591 
    592 fn decode_nibble(value: u8) -> Result<u8, RestoreMarkerContractError> {
    593     match value {
    594         b'0'..=b'9' => Ok(value - b'0'),
    595         b'a'..=b'f' => Ok(value - b'a' + 10),
    596         _ => Err(RestoreMarkerContractError::InvalidIdentity),
    597     }
    598 }
    599 
    600 #[cfg(any(target_os = "linux", target_os = "macos"))]
    601 mod store {
    602     use std::{
    603         fs::File,
    604         io::{Read, Seek, SeekFrom, Write},
    605     };
    606 
    607     use rustix::{
    608         fs::{
    609             AtFlags, FileType, Mode, OFlags, fchmod, fstat, open, openat, renameat, statat,
    610             unlinkat,
    611         },
    612         io::Errno,
    613         process::geteuid,
    614     };
    615 
    616     use super::*;
    617 
    618     #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    619     struct FileIdentity {
    620         device: u64,
    621         inode: u64,
    622     }
    623 
    624     pub(crate) struct RestoreMarkerBinding {
    625         directory: File,
    626         directory_identity: FileIdentity,
    627         marker_file: File,
    628         marker_identity: FileIdentity,
    629         marker: RestoreRecoveryMarker,
    630     }
    631 
    632     impl fmt::Debug for RestoreMarkerBinding {
    633         fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    634             formatter
    635                 .debug_struct("RestoreMarkerBinding")
    636                 .field("phase", &self.marker.phase())
    637                 .field("artifacts", &"[redacted]")
    638                 .finish()
    639         }
    640     }
    641 
    642     impl RestoreMarkerBinding {
    643         #[cfg(test)]
    644         pub(crate) fn create(
    645             paths: &ServiceSqlitePaths,
    646             authority: &WriterAuthority,
    647             marker: &RestoreRecoveryMarker,
    648         ) -> Result<Self, ServiceSqliteError> {
    649             Self::create_with_durable_callback(paths, authority, marker, || {})
    650         }
    651 
    652         pub(crate) fn create_with_durable_callback(
    653             paths: &ServiceSqlitePaths,
    654             authority: &WriterAuthority,
    655             marker: &RestoreRecoveryMarker,
    656             on_durable: impl FnOnce(),
    657         ) -> Result<Self, ServiceSqliteError> {
    658             let failpoints = crate::failpoint::DurabilityFailpoints::default();
    659             Self::create_with_durable_callback_and_failpoints(
    660                 paths,
    661                 authority,
    662                 marker,
    663                 &failpoints,
    664                 on_durable,
    665             )
    666         }
    667 
    668         pub(crate) fn create_with_durable_callback_and_failpoints(
    669             paths: &ServiceSqlitePaths,
    670             authority: &WriterAuthority,
    671             marker: &RestoreRecoveryMarker,
    672             failpoints: &crate::failpoint::DurabilityFailpoints,
    673             on_durable: impl FnOnce(),
    674         ) -> Result<Self, ServiceSqliteError> {
    675             Self::create_with_operations(
    676                 paths,
    677                 authority,
    678                 marker,
    679                 &SystemStoreOperations,
    680                 failpoints,
    681                 on_durable,
    682             )
    683         }
    684 
    685         #[cfg(test)]
    686         pub(crate) fn test_create_with_durable_authority_drift(
    687             paths: &ServiceSqlitePaths,
    688             authority: &WriterAuthority,
    689             marker: &RestoreRecoveryMarker,
    690             on_durable: impl FnOnce(),
    691         ) -> Result<Self, ServiceSqliteError> {
    692             Self::create_with_operations(
    693                 paths,
    694                 authority,
    695                 marker,
    696                 &AuthorityDriftAfterSyncStoreOperations,
    697                 &crate::failpoint::DurabilityFailpoints::default(),
    698                 on_durable,
    699             )
    700         }
    701 
    702         fn create_with_operations(
    703             paths: &ServiceSqlitePaths,
    704             authority: &WriterAuthority,
    705             marker: &RestoreRecoveryMarker,
    706             operations: &dyn StoreOperations,
    707             failpoints: &crate::failpoint::DurabilityFailpoints,
    708             on_durable: impl FnOnce(),
    709         ) -> Result<Self, ServiceSqliteError> {
    710             authority.validate_for(paths)?;
    711             require_marker_contract(
    712                 marker.matches_paths(paths),
    713                 RestoreMarkerContractError::InvalidIdentity,
    714             )
    715             .map_err(restore_contract)?;
    716             require_marker_contract(
    717                 marker.phase() == RestoreRecoveryPhase::Prepared,
    718                 RestoreMarkerContractError::IllegalTransition,
    719             )
    720             .map_err(restore_contract)?;
    721             let layout = RestoreRecoveryLayout::for_paths(paths).map_err(restore_contract)?;
    722             let directory = authority_checked(authority, paths, || {
    723                 authority
    724                     .directory()
    725                     .try_clone()
    726                     .map_err(|_| StoreFailure::Directory)
    727             })?
    728             .map_err(restore_store)?;
    729             let directory_identity =
    730                 authority_checked(authority, paths, || validate_directory(&directory))?
    731                     .map_err(restore_store)?;
    732             authority_checked(authority, paths, || {
    733                 require_absent(&directory, MARKER_NEXT_FILE_NAME)
    734             })?
    735             .map_err(restore_store)?;
    736             authority_checked(authority, paths, || {
    737                 hit(
    738                     failpoints,
    739                     crate::failpoint::DurabilityFailpoint::MarkerBeforeCreate,
    740                 )
    741             })?
    742             .map_err(restore_store)?;
    743             let (marker_file, marker_identity) = authority_checked(authority, paths, || {
    744                 let file = create_marker_file(&directory, MARKER_FILE_NAME)?;
    745                 let identity = file_identity(&file)?;
    746                 Ok::<_, StoreFailure>((file, identity))
    747             })?
    748             .map_err(restore_store)?;
    749             if let Err(cause) = hit(
    750                 failpoints,
    751                 crate::failpoint::DurabilityFailpoint::MarkerAfterCreate,
    752             ) {
    753                 cleanup_with_authority(
    754                     authority,
    755                     paths,
    756                     &directory,
    757                     MARKER_FILE_NAME,
    758                     marker_identity,
    759                 )?;
    760                 return Err(restore_store(cause));
    761             }
    762             let write_result = authority_checked(authority, paths, || {
    763                 write_and_sync(
    764                     &marker_file,
    765                     marker.canonical_bytes(),
    766                     &directory,
    767                     operations,
    768                     failpoints,
    769                     Some(crate::failpoint::DurabilityFailpoint::MarkerBeforeFileSync),
    770                     Some(crate::failpoint::DurabilityFailpoint::MarkerAfterFileSync),
    771                 )
    772             })?;
    773             if let Err(cause) = write_result {
    774                 cleanup_with_authority(
    775                     authority,
    776                     paths,
    777                     &directory,
    778                     MARKER_FILE_NAME,
    779                     marker_identity,
    780                 )?;
    781                 return Err(restore_store(cause));
    782             }
    783             authority.validate_for(paths)?;
    784             if let Err(cause) = hit(
    785                 failpoints,
    786                 crate::failpoint::DurabilityFailpoint::MarkerBeforeDirectorySync,
    787             ) {
    788                 cleanup_with_authority(
    789                     authority,
    790                     paths,
    791                     &directory,
    792                     MARKER_FILE_NAME,
    793                     marker_identity,
    794                 )?;
    795                 return Err(restore_store(cause));
    796             }
    797             if let Err(cause) = require_store_condition(
    798                 operations.sync_directory(&directory).is_ok(),
    799                 StoreFailure::Sync,
    800             ) {
    801                 authority.validate_for(paths)?;
    802                 cleanup_with_authority(
    803                     authority,
    804                     paths,
    805                     &directory,
    806                     MARKER_FILE_NAME,
    807                     marker_identity,
    808                 )?;
    809                 return Err(restore_store(cause));
    810             }
    811             // The marker contents and its directory entry are durable from
    812             // this point. The caller must transfer ownership of every bound
    813             // artifact before any subsequent fallible validation.
    814             on_durable();
    815             let after_directory_sync = hit(
    816                 failpoints,
    817                 crate::failpoint::DurabilityFailpoint::MarkerAfterDirectorySync,
    818             );
    819             authority.validate_for(paths)?;
    820             after_directory_sync.map_err(restore_store)?;
    821             let binding = Self {
    822                 directory,
    823                 directory_identity,
    824                 marker_file,
    825                 marker_identity,
    826                 marker: marker.clone(),
    827             };
    828             authority_checked(authority, paths, || binding.validate_for_restore(paths))??;
    829             require_marker_contract(
    830                 layout_uses_fixed_marker_name(&layout),
    831                 RestoreMarkerContractError::InvalidLayout,
    832             )
    833             .map_err(restore_contract)?;
    834             Ok(binding)
    835         }
    836 
    837         pub(crate) fn load(paths: &ServiceSqlitePaths) -> Result<Option<Self>, ServiceSqliteError> {
    838             let layout = RestoreRecoveryLayout::for_paths(paths).map_err(recovery_contract)?;
    839             let directory = open(
    840                 &layout.state_directory,
    841                 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
    842                 Mode::empty(),
    843             )
    844             .map_err(|_| recovery_store(StoreFailure::Directory))?;
    845             let directory = File::from(directory);
    846             let directory_identity = validate_directory(&directory).map_err(recovery_store)?;
    847             require_absent(&directory, MARKER_NEXT_FILE_NAME).map_err(recovery_store)?;
    848             let marker_file = match open_marker_file(&directory, MARKER_FILE_NAME) {
    849                 Ok(file) => file,
    850                 Err(StoreFailure::Missing) => return Ok(None),
    851                 Err(cause) => return Err(recovery_store(cause)),
    852             };
    853             let marker_identity = file_identity(&marker_file).map_err(recovery_store)?;
    854             let marker = read_marker(&marker_file).map_err(recovery_store)?;
    855             require_marker_contract(
    856                 marker.matches_paths(paths),
    857                 RestoreMarkerContractError::InvalidIdentity,
    858             )
    859             .map_err(recovery_contract)?;
    860             let binding = Self {
    861                 directory,
    862                 directory_identity,
    863                 marker_file,
    864                 marker_identity,
    865                 marker,
    866             };
    867             binding.validate(paths)?;
    868             Ok(Some(binding))
    869         }
    870 
    871         pub(crate) fn load_for_recovery(
    872             paths: &ServiceSqlitePaths,
    873             authority: &WriterAuthority,
    874         ) -> Result<Option<Self>, ServiceSqliteError> {
    875             authority.validate_for(paths)?;
    876             let layout = RestoreRecoveryLayout::for_paths(paths).map_err(recovery_contract)?;
    877             let directory = authority_checked(authority, paths, || {
    878                 authority
    879                     .directory()
    880                     .try_clone()
    881                     .map_err(|_| StoreFailure::Directory)
    882             })?
    883             .map_err(recovery_store)?;
    884             let directory_identity =
    885                 authority_checked(authority, paths, || validate_directory(&directory))?
    886                     .map_err(authority_store)?;
    887             let marker_file = match authority_checked(authority, paths, || {
    888                 open_marker_file(&directory, MARKER_FILE_NAME)
    889             })? {
    890                 Ok(file) => file,
    891                 Err(StoreFailure::Missing) => {
    892                     authority_checked(authority, paths, || {
    893                         require_absent(&directory, MARKER_NEXT_FILE_NAME)
    894                     })?
    895                     .map_err(recovery_store)?;
    896                     return Ok(None);
    897                 }
    898                 Err(cause) => return Err(recovery_store(cause)),
    899             };
    900             let marker_identity =
    901                 authority_checked(authority, paths, || file_identity(&marker_file))?
    902                     .map_err(recovery_store)?;
    903             let marker = authority_checked(authority, paths, || read_marker(&marker_file))?
    904                 .map_err(recovery_store)?;
    905             require_marker_contract(
    906                 marker.matches_paths(paths),
    907                 RestoreMarkerContractError::InvalidIdentity,
    908             )
    909             .map_err(recovery_contract)?;
    910             let binding = Self {
    911                 directory,
    912                 directory_identity,
    913                 marker_file,
    914                 marker_identity,
    915                 marker,
    916             };
    917             authority_checked(authority, paths, || {
    918                 binding.validate_inner(paths, false, ServiceSqliteErrorKind::Recovery)
    919             })??;
    920             require_marker_contract(
    921                 layout_uses_fixed_marker_name(&layout),
    922                 RestoreMarkerContractError::InvalidLayout,
    923             )
    924             .map_err(recovery_contract)?;
    925             authority.validate_for(paths)?;
    926             Ok(Some(binding))
    927         }
    928 
    929         pub(crate) fn advance(
    930             self,
    931             paths: &ServiceSqlitePaths,
    932             authority: &WriterAuthority,
    933             next: RestoreRecoveryPhase,
    934         ) -> Result<Self, ServiceSqliteError> {
    935             let failpoints = crate::failpoint::DurabilityFailpoints::default();
    936             self.advance_with_failpoints(paths, authority, next, &failpoints)
    937         }
    938 
    939         pub(crate) fn advance_with_failpoints(
    940             self,
    941             paths: &ServiceSqlitePaths,
    942             authority: &WriterAuthority,
    943             next: RestoreRecoveryPhase,
    944             failpoints: &crate::failpoint::DurabilityFailpoints,
    945         ) -> Result<Self, ServiceSqliteError> {
    946             self.advance_with_operations(
    947                 paths,
    948                 authority,
    949                 next,
    950                 &SystemStoreOperations,
    951                 ServiceSqliteErrorKind::Restore,
    952                 failpoints,
    953             )
    954         }
    955 
    956         pub(crate) fn advance_for_recovery(
    957             self,
    958             paths: &ServiceSqlitePaths,
    959             authority: &WriterAuthority,
    960             next: RestoreRecoveryPhase,
    961         ) -> Result<Self, ServiceSqliteError> {
    962             self.advance_with_operations(
    963                 paths,
    964                 authority,
    965                 next,
    966                 &SystemStoreOperations,
    967                 ServiceSqliteErrorKind::Recovery,
    968                 &crate::failpoint::DurabilityFailpoints::default(),
    969             )
    970         }
    971 
    972         fn advance_with_operations(
    973             self,
    974             paths: &ServiceSqlitePaths,
    975             authority: &WriterAuthority,
    976             next: RestoreRecoveryPhase,
    977             operations: &dyn StoreOperations,
    978             operation_kind: ServiceSqliteErrorKind,
    979             failpoints: &crate::failpoint::DurabilityFailpoints,
    980         ) -> Result<Self, ServiceSqliteError> {
    981             authority_checked(authority, paths, || {
    982                 self.validate_inner(paths, true, operation_kind)
    983             })??;
    984             let current = authority_checked(authority, paths, || read_marker(&self.marker_file))?
    985                 .map_err(|cause| operation_store(operation_kind, cause))?;
    986             require_store_condition(
    987                 current.canonical_bytes() == self.marker.canonical_bytes(),
    988                 StoreFailure::Conflict,
    989             )
    990             .map_err(|cause| operation_store(operation_kind, cause))?;
    991             let next_marker = self
    992                 .marker
    993                 .transitioned_to(next)
    994                 .map_err(|cause| operation_contract(operation_kind, cause))?;
    995             if next_marker.canonical_bytes() == self.marker.canonical_bytes() {
    996                 return Ok(self);
    997             }
    998             authority_checked(authority, paths, || {
    999                 require_absent(&self.directory, MARKER_NEXT_FILE_NAME)
   1000             })?
   1001             .map_err(|cause| operation_store(operation_kind, cause))?;
   1002             let (scratch, scratch_identity) = authority_checked(authority, paths, || {
   1003                 let file = create_marker_file(&self.directory, MARKER_NEXT_FILE_NAME)?;
   1004                 let identity = file_identity(&file)?;
   1005                 Ok::<_, StoreFailure>((file, identity))
   1006             })?
   1007             .map_err(|cause| operation_store(operation_kind, cause))?;
   1008             let before_write = authority_checked(authority, paths, || {
   1009                 hit(
   1010                     failpoints,
   1011                     crate::failpoint::DurabilityFailpoint::MarkerAdvanceBeforeWriteAndFileSync,
   1012                 )
   1013             })?;
   1014             if let Err(cause) = before_write {
   1015                 cleanup_with_authority(
   1016                     authority,
   1017                     paths,
   1018                     &self.directory,
   1019                     MARKER_NEXT_FILE_NAME,
   1020                     scratch_identity,
   1021                 )?;
   1022                 return Err(operation_store(operation_kind, cause));
   1023             }
   1024             let scratch_write = authority_checked(authority, paths, || {
   1025                 write_and_sync(
   1026                     &scratch,
   1027                     next_marker.canonical_bytes(),
   1028                     &self.directory,
   1029                     operations,
   1030                     failpoints,
   1031                     None,
   1032                     None,
   1033                 )
   1034             })?;
   1035             if let Err(cause) = scratch_write {
   1036                 cleanup_with_authority(
   1037                     authority,
   1038                     paths,
   1039                     &self.directory,
   1040                     MARKER_NEXT_FILE_NAME,
   1041                     scratch_identity,
   1042                 )?;
   1043                 return Err(operation_store(operation_kind, cause));
   1044             }
   1045             authority_checked(authority, paths, || {
   1046                 hit(
   1047                     failpoints,
   1048                     crate::failpoint::DurabilityFailpoint::MarkerAdvanceAfterWriteAndFileSync,
   1049                 )
   1050             })?
   1051             .map_err(|cause| operation_store(operation_kind, cause))?;
   1052             authority_checked(authority, paths, || {
   1053                 self.validate_inner(paths, false, operation_kind)
   1054             })??;
   1055             let scratch_matches = authority_checked(authority, paths, || {
   1056                 let current = open_marker_file(&self.directory, MARKER_NEXT_FILE_NAME)?;
   1057                 Ok::<_, StoreFailure>(
   1058                     file_identity(&scratch)? == scratch_identity
   1059                         && file_identity(&current)? == scratch_identity,
   1060                 )
   1061             })?
   1062             .map_err(|cause| operation_store(operation_kind, cause))?;
   1063             if let Err(cause) = require_store_condition(scratch_matches, StoreFailure::Conflict) {
   1064                 cleanup_with_authority(
   1065                     authority,
   1066                     paths,
   1067                     &self.directory,
   1068                     MARKER_NEXT_FILE_NAME,
   1069                     scratch_identity,
   1070                 )?;
   1071                 return Err(operation_store(operation_kind, cause));
   1072             }
   1073             let before_replace = authority_checked(authority, paths, || {
   1074                 hit(
   1075                     failpoints,
   1076                     crate::failpoint::DurabilityFailpoint::MarkerAdvanceBeforeReplace,
   1077                 )
   1078             })?;
   1079             if let Err(cause) = before_replace {
   1080                 cleanup_with_authority(
   1081                     authority,
   1082                     paths,
   1083                     &self.directory,
   1084                     MARKER_NEXT_FILE_NAME,
   1085                     scratch_identity,
   1086                 )?;
   1087                 return Err(operation_store(operation_kind, cause));
   1088             }
   1089             let replacement = authority_checked(authority, paths, || {
   1090                 operations
   1091                     .replace_marker(&self.directory)
   1092                     .map_err(|_| StoreFailure::Rename)
   1093             })?;
   1094             if let Err(cause) = require_store_condition(replacement.is_ok(), StoreFailure::Rename) {
   1095                 cleanup_with_authority(
   1096                     authority,
   1097                     paths,
   1098                     &self.directory,
   1099                     MARKER_NEXT_FILE_NAME,
   1100                     scratch_identity,
   1101                 )?;
   1102                 return Err(operation_store(operation_kind, cause));
   1103             }
   1104             authority_checked(authority, paths, || {
   1105                 hit(
   1106                     failpoints,
   1107                     crate::failpoint::DurabilityFailpoint::MarkerAdvanceAfterReplace,
   1108                 )
   1109             })?
   1110             .map_err(|cause| operation_store(operation_kind, cause))?;
   1111             authority_checked(authority, paths, || {
   1112                 hit(
   1113                     failpoints,
   1114                     crate::failpoint::DurabilityFailpoint::MarkerAdvanceBeforeDirectorySync,
   1115                 )
   1116             })?
   1117             .map_err(|cause| operation_store(operation_kind, cause))?;
   1118             let parent_sync = authority_checked(authority, paths, || {
   1119                 operations
   1120                     .sync_directory(&self.directory)
   1121                     .map_err(|_| StoreFailure::Sync)
   1122             })?;
   1123             require_store_condition(parent_sync.is_ok(), StoreFailure::Sync)
   1124                 .map_err(|cause| operation_store(operation_kind, cause))?;
   1125             authority_checked(authority, paths, || {
   1126                 hit(
   1127                     failpoints,
   1128                     crate::failpoint::DurabilityFailpoint::MarkerAdvanceAfterDirectorySync,
   1129                 )
   1130             })?
   1131             .map_err(|cause| operation_store(operation_kind, cause))?;
   1132             let (marker_file, marker_identity, reread) =
   1133                 authority_checked(authority, paths, || {
   1134                     let file = open_marker_file(&self.directory, MARKER_FILE_NAME)?;
   1135                     let identity = file_identity(&file)?;
   1136                     let marker = read_marker(&file)?;
   1137                     Ok::<_, StoreFailure>((file, identity, marker))
   1138                 })?
   1139                 .map_err(|cause| operation_store(operation_kind, cause))?;
   1140             require_store_condition(
   1141                 reread.canonical_bytes() == next_marker.canonical_bytes(),
   1142                 StoreFailure::Conflict,
   1143             )
   1144             .map_err(|cause| operation_store(operation_kind, cause))?;
   1145             let binding = Self {
   1146                 directory: self.directory,
   1147                 directory_identity: self.directory_identity,
   1148                 marker_file,
   1149                 marker_identity,
   1150                 marker: next_marker,
   1151             };
   1152             authority_checked(authority, paths, || {
   1153                 binding.validate_inner(paths, true, operation_kind)
   1154             })??;
   1155             Ok(binding)
   1156         }
   1157 
   1158         #[cfg(test)]
   1159         pub(crate) fn test_advance_with_failure(
   1160             self,
   1161             paths: &ServiceSqlitePaths,
   1162             authority: &WriterAuthority,
   1163             next: RestoreRecoveryPhase,
   1164             failure: TestStoreFailure,
   1165         ) -> Result<Self, ServiceSqliteError> {
   1166             self.advance_with_operations(
   1167                 paths,
   1168                 authority,
   1169                 next,
   1170                 &FailingStoreOperations { failure },
   1171                 ServiceSqliteErrorKind::Restore,
   1172                 &crate::failpoint::DurabilityFailpoints::default(),
   1173             )
   1174         }
   1175 
   1176         #[cfg(test)]
   1177         pub(crate) fn test_advance_for_recovery_with_failure(
   1178             self,
   1179             paths: &ServiceSqlitePaths,
   1180             authority: &WriterAuthority,
   1181             next: RestoreRecoveryPhase,
   1182             failure: TestStoreFailure,
   1183         ) -> Result<Self, ServiceSqliteError> {
   1184             self.advance_with_operations(
   1185                 paths,
   1186                 authority,
   1187                 next,
   1188                 &FailingStoreOperations { failure },
   1189                 ServiceSqliteErrorKind::Recovery,
   1190                 &crate::failpoint::DurabilityFailpoints::default(),
   1191             )
   1192         }
   1193 
   1194         pub(crate) const fn marker(&self) -> &RestoreRecoveryMarker {
   1195             &self.marker
   1196         }
   1197 
   1198         pub(crate) fn interrupted_transition(
   1199             &self,
   1200             paths: &ServiceSqlitePaths,
   1201             authority: &WriterAuthority,
   1202         ) -> Result<Option<RestoreRecoveryPhase>, ServiceSqliteError> {
   1203             authority_checked(authority, paths, || {
   1204                 self.validate_inner(paths, false, ServiceSqliteErrorKind::Recovery)
   1205             })??;
   1206             let scratch = match authority_checked(authority, paths, || {
   1207                 open_marker_file(&self.directory, MARKER_NEXT_FILE_NAME)
   1208             })? {
   1209                 Ok(file) => file,
   1210                 Err(StoreFailure::Missing) => return Ok(None),
   1211                 Err(cause) => return Err(recovery_store(cause)),
   1212             };
   1213             let scratch_marker = authority_checked(authority, paths, || read_marker(&scratch))?
   1214                 .map_err(recovery_store)?;
   1215             require_store_condition(
   1216                 interrupted_successor_matches(&self.marker, &scratch_marker),
   1217                 StoreFailure::Conflict,
   1218             )
   1219             .map_err(recovery_store)?;
   1220             let next = scratch_marker.phase();
   1221             Ok(Some(next))
   1222         }
   1223 
   1224         pub(crate) fn promote_interrupted_transition(
   1225             self,
   1226             paths: &ServiceSqlitePaths,
   1227             authority: &WriterAuthority,
   1228             expected_phase: RestoreRecoveryPhase,
   1229         ) -> Result<Self, ServiceSqliteError> {
   1230             self.promote_interrupted_transition_with_hook(paths, authority, expected_phase, || {})
   1231         }
   1232 
   1233         fn promote_interrupted_transition_with_hook(
   1234             self,
   1235             paths: &ServiceSqlitePaths,
   1236             authority: &WriterAuthority,
   1237             expected_phase: RestoreRecoveryPhase,
   1238             before_exact_removal: impl FnOnce(),
   1239         ) -> Result<Self, ServiceSqliteError> {
   1240             authority.validate_for(paths)?;
   1241             authority_checked(authority, paths, || {
   1242                 self.validate_inner(paths, false, ServiceSqliteErrorKind::Recovery)
   1243             })??;
   1244             let scratch = authority_checked(authority, paths, || {
   1245                 open_marker_file(&self.directory, MARKER_NEXT_FILE_NAME)
   1246             })?
   1247             .map_err(recovery_store)?;
   1248             let scratch_identity = authority_checked(authority, paths, || file_identity(&scratch))?
   1249                 .map_err(recovery_store)?;
   1250             let scratch_marker = authority_checked(authority, paths, || read_marker(&scratch))?
   1251                 .map_err(recovery_store)?;
   1252             let expected = self
   1253                 .marker
   1254                 .transitioned_to(expected_phase)
   1255                 .map_err(recovery_contract)?;
   1256             require_store_condition(
   1257                 scratch_marker.canonical_bytes() == expected.canonical_bytes(),
   1258                 StoreFailure::Conflict,
   1259             )
   1260             .map_err(recovery_store)?;
   1261             before_exact_removal();
   1262             // Preserve the valid current marker even if the scratch pathname
   1263             // was replaced after an interrupted advance. Remove only the
   1264             // exact validated scratch, then recreate the governed transition.
   1265             let removal = authority_checked(authority, paths, || {
   1266                 remove_exact_and_sync(&self.directory, MARKER_NEXT_FILE_NAME, scratch_identity)
   1267             })?;
   1268             removal.map_err(recovery_store)?;
   1269             self.advance_for_recovery(paths, authority, expected_phase)
   1270         }
   1271 
   1272         #[cfg(test)]
   1273         pub(crate) fn test_promote_interrupted_transition_after_hook(
   1274             self,
   1275             paths: &ServiceSqlitePaths,
   1276             authority: &WriterAuthority,
   1277             expected_phase: RestoreRecoveryPhase,
   1278             before_exact_removal: impl FnOnce(),
   1279         ) -> Result<Self, ServiceSqliteError> {
   1280             self.promote_interrupted_transition_with_hook(
   1281                 paths,
   1282                 authority,
   1283                 expected_phase,
   1284                 before_exact_removal,
   1285             )
   1286         }
   1287 
   1288         pub(crate) fn retire(
   1289             self,
   1290             paths: &ServiceSqlitePaths,
   1291             authority: &WriterAuthority,
   1292         ) -> Result<(), ServiceSqliteError> {
   1293             authority.validate_for(paths)?;
   1294             authority_checked(authority, paths, || {
   1295                 self.validate_inner(paths, true, ServiceSqliteErrorKind::Recovery)
   1296             })??;
   1297             authority_checked(authority, paths, || {
   1298                 remove_exact_and_sync(&self.directory, MARKER_FILE_NAME, self.marker_identity)
   1299             })?
   1300             .map_err(recovery_store)
   1301         }
   1302 
   1303         pub(crate) fn validate(
   1304             &self,
   1305             paths: &ServiceSqlitePaths,
   1306         ) -> Result<(), ServiceSqliteError> {
   1307             self.validate_inner(paths, true, ServiceSqliteErrorKind::Recovery)
   1308         }
   1309 
   1310         fn validate_for_restore(
   1311             &self,
   1312             paths: &ServiceSqlitePaths,
   1313         ) -> Result<(), ServiceSqliteError> {
   1314             self.validate_inner(paths, true, ServiceSqliteErrorKind::Restore)
   1315         }
   1316 
   1317         fn validate_inner(
   1318             &self,
   1319             paths: &ServiceSqlitePaths,
   1320             require_no_scratch: bool,
   1321             operation_kind: ServiceSqliteErrorKind,
   1322         ) -> Result<(), ServiceSqliteError> {
   1323             let layout = RestoreRecoveryLayout::for_paths(paths)
   1324                 .map_err(|cause| operation_contract(operation_kind, cause))?;
   1325             let current_directory = open(
   1326                 &layout.state_directory,
   1327                 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
   1328                 Mode::empty(),
   1329             )
   1330             .map_err(|_| authority_store(StoreFailure::Directory))?;
   1331             let current_directory = File::from(current_directory);
   1332             require_store_condition(
   1333                 validate_directory(&self.directory).map_err(authority_store)?
   1334                     == self.directory_identity,
   1335                 StoreFailure::Conflict,
   1336             )
   1337             .map_err(authority_store)?;
   1338             require_store_condition(
   1339                 validate_directory(&current_directory).map_err(authority_store)?
   1340                     == self.directory_identity,
   1341                 StoreFailure::Conflict,
   1342             )
   1343             .map_err(authority_store)?;
   1344             let current_marker = open_marker_file(&current_directory, MARKER_FILE_NAME)
   1345                 .map_err(|cause| operation_store(operation_kind, cause))?;
   1346             require_store_condition(
   1347                 file_identity(&self.marker_file)
   1348                     .map_err(|cause| operation_store(operation_kind, cause))?
   1349                     == self.marker_identity,
   1350                 StoreFailure::Conflict,
   1351             )
   1352             .map_err(|cause| operation_store(operation_kind, cause))?;
   1353             require_store_condition(
   1354                 file_identity(&current_marker)
   1355                     .map_err(|cause| operation_store(operation_kind, cause))?
   1356                     == self.marker_identity,
   1357                 StoreFailure::Conflict,
   1358             )
   1359             .map_err(|cause| operation_store(operation_kind, cause))?;
   1360             let marker_bytes_match = read_marker(&self.marker_file)
   1361                 .map_err(|cause| operation_store(operation_kind, cause))?
   1362                 .canonical_bytes()
   1363                 == self.marker.canonical_bytes();
   1364             require_store_condition(marker_bytes_match, StoreFailure::Conflict)
   1365                 .map_err(|cause| operation_store(operation_kind, cause))?;
   1366             if require_no_scratch {
   1367                 require_absent(&current_directory, MARKER_NEXT_FILE_NAME)
   1368                     .map_err(|cause| operation_store(operation_kind, cause))?;
   1369             }
   1370             Ok(())
   1371         }
   1372     }
   1373 
   1374     fn validate_directory(file: &File) -> Result<FileIdentity, StoreFailure> {
   1375         let status = fstat(file).map_err(|_| StoreFailure::Directory)?;
   1376         if !crate::native_metadata::secure_directory(
   1377             FileType::from_raw_mode(status.st_mode).is_dir(),
   1378             status.st_uid,
   1379             geteuid().as_raw(),
   1380             crate::native_metadata::mode(status.st_mode),
   1381         ) {
   1382             return Err(StoreFailure::Directory);
   1383         }
   1384         identity(status.st_dev, status.st_ino)
   1385     }
   1386 
   1387     fn authority_checked<T, E>(
   1388         authority: &WriterAuthority,
   1389         paths: &ServiceSqlitePaths,
   1390         operation: impl FnOnce() -> Result<T, E>,
   1391     ) -> Result<Result<T, E>, ServiceSqliteError> {
   1392         authority.validate_for(paths)?;
   1393         let result = operation();
   1394         authority.validate_for(paths)?;
   1395         Ok(result)
   1396     }
   1397 
   1398     fn cleanup_with_authority(
   1399         authority: &WriterAuthority,
   1400         paths: &ServiceSqlitePaths,
   1401         directory: &File,
   1402         name: &str,
   1403         expected: FileIdentity,
   1404     ) -> Result<(), ServiceSqliteError> {
   1405         authority.validate_for(paths)?;
   1406         cleanup_exact(directory, name, expected);
   1407         authority.validate_for(paths)
   1408     }
   1409 
   1410     fn create_marker_file(directory: &File, name: &str) -> Result<File, StoreFailure> {
   1411         let descriptor = openat(
   1412             directory,
   1413             name,
   1414             OFlags::RDWR
   1415                 | OFlags::CREATE
   1416                 | OFlags::EXCL
   1417                 | OFlags::NOFOLLOW
   1418                 | OFlags::CLOEXEC
   1419                 | OFlags::NONBLOCK,
   1420             Mode::RUSR | Mode::WUSR,
   1421         )
   1422         .map_err(|_| StoreFailure::Collision)?;
   1423         fchmod(&descriptor, Mode::RUSR | Mode::WUSR).map_err(|_| StoreFailure::Permissions)?;
   1424         let file = File::from(descriptor);
   1425         let _ = file_identity(&file)?;
   1426         Ok(file)
   1427     }
   1428 
   1429     fn open_marker_file(directory: &File, name: &str) -> Result<File, StoreFailure> {
   1430         let descriptor = openat(
   1431             directory,
   1432             name,
   1433             OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC | OFlags::NONBLOCK,
   1434             Mode::empty(),
   1435         )
   1436         .map_err(|error| {
   1437             if error == Errno::NOENT {
   1438                 StoreFailure::Missing
   1439             } else {
   1440                 StoreFailure::Marker
   1441             }
   1442         })?;
   1443         let file = File::from(descriptor);
   1444         let _ = file_identity(&file)?;
   1445         Ok(file)
   1446     }
   1447 
   1448     fn file_identity(file: &File) -> Result<FileIdentity, StoreFailure> {
   1449         let status = fstat(file).map_err(|_| StoreFailure::Marker)?;
   1450         if !crate::native_metadata::exact_regular_file(
   1451             FileType::from_raw_mode(status.st_mode).is_file(),
   1452             crate::native_metadata::link_count(status.st_nlink),
   1453             status.st_uid,
   1454             geteuid().as_raw(),
   1455             crate::native_metadata::mode(status.st_mode),
   1456         ) {
   1457             return Err(StoreFailure::Marker);
   1458         }
   1459         identity(status.st_dev, status.st_ino)
   1460     }
   1461 
   1462     fn identity(device: impl TryInto<u64>, inode: u64) -> Result<FileIdentity, StoreFailure> {
   1463         Ok(FileIdentity {
   1464             device: device.try_into().map_err(|_| StoreFailure::Marker)?,
   1465             inode,
   1466         })
   1467     }
   1468 
   1469     trait StoreOperations {
   1470         fn sync_file(&self, file: &File, directory: &File) -> std::io::Result<()>;
   1471         fn sync_directory(&self, directory: &File) -> std::io::Result<()>;
   1472         fn replace_marker(&self, directory: &File) -> std::io::Result<()>;
   1473     }
   1474 
   1475     struct SystemStoreOperations;
   1476 
   1477     impl StoreOperations for SystemStoreOperations {
   1478         #[cfg_attr(coverage_nightly, coverage(off))]
   1479         fn sync_file(&self, file: &File, _directory: &File) -> std::io::Result<()> {
   1480             file.sync_all()
   1481         }
   1482 
   1483         #[cfg_attr(coverage_nightly, coverage(off))]
   1484         fn sync_directory(&self, directory: &File) -> std::io::Result<()> {
   1485             directory.sync_all()
   1486         }
   1487 
   1488         #[cfg_attr(coverage_nightly, coverage(off))]
   1489         fn replace_marker(&self, directory: &File) -> std::io::Result<()> {
   1490             renameat(
   1491                 directory,
   1492                 MARKER_NEXT_FILE_NAME,
   1493                 directory,
   1494                 MARKER_FILE_NAME,
   1495             )
   1496             .map_err(std::io::Error::from)
   1497         }
   1498     }
   1499 
   1500     #[cfg(test)]
   1501     struct AuthorityDriftAfterSyncStoreOperations;
   1502 
   1503     #[cfg(test)]
   1504     impl StoreOperations for AuthorityDriftAfterSyncStoreOperations {
   1505         fn sync_file(&self, file: &File, _directory: &File) -> std::io::Result<()> {
   1506             file.sync_all()
   1507         }
   1508 
   1509         fn sync_directory(&self, directory: &File) -> std::io::Result<()> {
   1510             directory.sync_all()?;
   1511             fchmod(
   1512                 directory,
   1513                 Mode::RUSR | Mode::WUSR | Mode::XUSR | Mode::RGRP | Mode::WGRP | Mode::XGRP,
   1514             )
   1515             .map_err(std::io::Error::from)
   1516         }
   1517 
   1518         fn replace_marker(&self, directory: &File) -> std::io::Result<()> {
   1519             SystemStoreOperations.replace_marker(directory)
   1520         }
   1521     }
   1522 
   1523     #[cfg(test)]
   1524     #[derive(Clone, Copy, Debug, PartialEq, Eq)]
   1525     pub(crate) enum TestStoreFailure {
   1526         ScratchSync,
   1527         ParentSyncAfterRename,
   1528         AuthorityDriftAndScratchSync,
   1529     }
   1530 
   1531     #[cfg(test)]
   1532     struct FailingStoreOperations {
   1533         failure: TestStoreFailure,
   1534     }
   1535 
   1536     #[cfg(test)]
   1537     impl StoreOperations for FailingStoreOperations {
   1538         fn sync_file(&self, file: &File, directory: &File) -> std::io::Result<()> {
   1539             match self.failure {
   1540                 TestStoreFailure::ScratchSync => Err(crate::failpoint::storage_full_error()),
   1541                 TestStoreFailure::AuthorityDriftAndScratchSync => {
   1542                     fchmod(
   1543                         directory,
   1544                         Mode::RUSR | Mode::WUSR | Mode::XUSR | Mode::RGRP | Mode::WGRP | Mode::XGRP,
   1545                     )
   1546                     .map_err(std::io::Error::from)?;
   1547                     Err(crate::failpoint::storage_full_error())
   1548                 }
   1549                 TestStoreFailure::ParentSyncAfterRename => file.sync_all(),
   1550             }
   1551         }
   1552 
   1553         fn sync_directory(&self, _directory: &File) -> std::io::Result<()> {
   1554             Err(crate::failpoint::storage_full_error())
   1555         }
   1556 
   1557         fn replace_marker(&self, directory: &File) -> std::io::Result<()> {
   1558             SystemStoreOperations.replace_marker(directory)
   1559         }
   1560     }
   1561 
   1562     fn write_and_sync(
   1563         file: &File,
   1564         bytes: &[u8],
   1565         directory: &File,
   1566         operations: &dyn StoreOperations,
   1567         failpoints: &crate::failpoint::DurabilityFailpoints,
   1568         before_sync: Option<crate::failpoint::DurabilityFailpoint>,
   1569         after_sync: Option<crate::failpoint::DurabilityFailpoint>,
   1570     ) -> Result<(), StoreFailure> {
   1571         let mut file = file.try_clone().map_err(|_| StoreFailure::Write)?;
   1572         file.write_all(bytes).map_err(|_| StoreFailure::Write)?;
   1573         if let Some(before_sync) = before_sync {
   1574             hit(failpoints, before_sync)?;
   1575         }
   1576         operations
   1577             .sync_file(&file, directory)
   1578             .map_err(|_| StoreFailure::Sync)?;
   1579         if let Some(after_sync) = after_sync {
   1580             hit(failpoints, after_sync)?;
   1581         }
   1582         Ok(())
   1583     }
   1584 
   1585     fn hit(
   1586         failpoints: &crate::failpoint::DurabilityFailpoints,
   1587         point: crate::failpoint::DurabilityFailpoint,
   1588     ) -> Result<(), StoreFailure> {
   1589         failpoints.hit(point).map_err(|_| StoreFailure::Injected)
   1590     }
   1591 
   1592     fn read_marker(file: &File) -> Result<RestoreRecoveryMarker, StoreFailure> {
   1593         let mut file = file.try_clone().map_err(|_| StoreFailure::Read)?;
   1594         file.seek(SeekFrom::Start(0))
   1595             .map_err(|_| StoreFailure::Read)?;
   1596         let mut bytes = Vec::with_capacity(RESTORE_MARKER_MAX_BYTES.min(512));
   1597         file.take(u64::try_from(RESTORE_MARKER_MAX_BYTES + 1).expect("marker bound"))
   1598             .read_to_end(&mut bytes)
   1599             .map_err(|_| StoreFailure::Read)?;
   1600         if bytes.len() > RESTORE_MARKER_MAX_BYTES {
   1601             return Err(StoreFailure::Contract(
   1602                 RestoreMarkerContractError::MarkerTooLarge,
   1603             ));
   1604         }
   1605         RestoreRecoveryMarker::from_canonical_bytes(&bytes).map_err(StoreFailure::Contract)
   1606     }
   1607 
   1608     fn require_absent(directory: &File, name: &str) -> Result<(), StoreFailure> {
   1609         match statat(directory, name, AtFlags::SYMLINK_NOFOLLOW) {
   1610             Err(error) if error == Errno::NOENT => Ok(()),
   1611             _ => Err(StoreFailure::Collision),
   1612         }
   1613     }
   1614 
   1615     fn cleanup_exact(directory: &File, name: &str, expected: FileIdentity) {
   1616         let Ok(file) = open_marker_file(directory, name) else {
   1617             return;
   1618         };
   1619         if file_identity(&file).ok() == Some(expected) {
   1620             let _ = unlinkat(directory, name, AtFlags::empty());
   1621             let _ = directory.sync_all();
   1622         }
   1623     }
   1624 
   1625     fn remove_exact_and_sync(
   1626         directory: &File,
   1627         name: &str,
   1628         expected: FileIdentity,
   1629     ) -> Result<(), StoreFailure> {
   1630         let current = open_marker_file(directory, name)?;
   1631         require_store_condition(file_identity(&current)? == expected, StoreFailure::Conflict)?;
   1632         unlinkat(directory, name, AtFlags::empty()).map_err(|_| StoreFailure::Conflict)?;
   1633         directory.sync_all().map_err(|_| StoreFailure::Sync)?;
   1634         require_absent(directory, name)
   1635     }
   1636 
   1637     #[derive(Clone, Copy, Debug, PartialEq, Eq)]
   1638     enum StoreFailure {
   1639         Directory,
   1640         Marker,
   1641         Missing,
   1642         Collision,
   1643         Permissions,
   1644         Read,
   1645         Write,
   1646         Sync,
   1647         Rename,
   1648         Conflict,
   1649         Injected,
   1650         Contract(RestoreMarkerContractError),
   1651     }
   1652 
   1653     fn require_store_condition(condition: bool, error: StoreFailure) -> Result<(), StoreFailure> {
   1654         if condition { Ok(()) } else { Err(error) }
   1655     }
   1656 
   1657     impl fmt::Display for StoreFailure {
   1658         fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
   1659             formatter.write_str(match self {
   1660                 Self::Directory => "restore marker directory is invalid",
   1661                 Self::Marker => "restore marker file is invalid",
   1662                 Self::Missing => "restore marker file is missing",
   1663                 Self::Collision => "restore marker artifact already exists",
   1664                 Self::Permissions => "restore marker permissions are invalid",
   1665                 Self::Read => "restore marker could not be read",
   1666                 Self::Write => "restore marker could not be written",
   1667                 Self::Sync => "restore marker durability could not be proven",
   1668                 Self::Rename => "restore marker replacement failed",
   1669                 Self::Conflict => "restore marker binding changed",
   1670                 Self::Injected => "restore marker durability boundary failed",
   1671                 Self::Contract(error) => return error.fmt(formatter),
   1672             })
   1673         }
   1674     }
   1675     impl Error for StoreFailure {}
   1676 
   1677     fn restore_store(cause: StoreFailure) -> ServiceSqliteError {
   1678         ServiceSqliteError::with_source(ServiceSqliteErrorKind::Restore, cause)
   1679     }
   1680     fn recovery_store(cause: StoreFailure) -> ServiceSqliteError {
   1681         ServiceSqliteError::with_source(ServiceSqliteErrorKind::Recovery, cause)
   1682     }
   1683     fn authority_store(cause: StoreFailure) -> ServiceSqliteError {
   1684         ServiceSqliteError::with_source(ServiceSqliteErrorKind::Authority, cause)
   1685     }
   1686     fn restore_contract(cause: RestoreMarkerContractError) -> ServiceSqliteError {
   1687         ServiceSqliteError::with_source(ServiceSqliteErrorKind::Restore, cause)
   1688     }
   1689     fn recovery_contract(cause: RestoreMarkerContractError) -> ServiceSqliteError {
   1690         ServiceSqliteError::with_source(ServiceSqliteErrorKind::Recovery, cause)
   1691     }
   1692     fn operation_store(kind: ServiceSqliteErrorKind, cause: StoreFailure) -> ServiceSqliteError {
   1693         debug_assert!(matches!(
   1694             kind,
   1695             ServiceSqliteErrorKind::Restore | ServiceSqliteErrorKind::Recovery
   1696         ));
   1697         ServiceSqliteError::with_source(kind, cause)
   1698     }
   1699     fn operation_contract(
   1700         kind: ServiceSqliteErrorKind,
   1701         cause: RestoreMarkerContractError,
   1702     ) -> ServiceSqliteError {
   1703         debug_assert!(matches!(
   1704             kind,
   1705             ServiceSqliteErrorKind::Restore | ServiceSqliteErrorKind::Recovery
   1706         ));
   1707         ServiceSqliteError::with_source(kind, cause)
   1708     }
   1709 
   1710     #[cfg(test)]
   1711     mod failure_tests {
   1712 
   1713         use super::*;
   1714 
   1715         #[test]
   1716         fn store_failure_inventory_is_complete_and_source_free() {
   1717             let contract = RestoreMarkerContractError::ChecksumMismatch;
   1718             for (failure, message) in [
   1719                 (
   1720                     StoreFailure::Directory,
   1721                     "restore marker directory is invalid",
   1722                 ),
   1723                 (StoreFailure::Marker, "restore marker file is invalid"),
   1724                 (StoreFailure::Missing, "restore marker file is missing"),
   1725                 (
   1726                     StoreFailure::Collision,
   1727                     "restore marker artifact already exists",
   1728                 ),
   1729                 (
   1730                     StoreFailure::Permissions,
   1731                     "restore marker permissions are invalid",
   1732                 ),
   1733                 (StoreFailure::Read, "restore marker could not be read"),
   1734                 (StoreFailure::Write, "restore marker could not be written"),
   1735                 (
   1736                     StoreFailure::Sync,
   1737                     "restore marker durability could not be proven",
   1738                 ),
   1739                 (StoreFailure::Rename, "restore marker replacement failed"),
   1740                 (StoreFailure::Conflict, "restore marker binding changed"),
   1741                 (
   1742                     StoreFailure::Injected,
   1743                     "restore marker durability boundary failed",
   1744                 ),
   1745                 (
   1746                     StoreFailure::Contract(contract),
   1747                     "restore marker checksum does not match",
   1748                 ),
   1749             ] {
   1750                 assert_eq!(failure.to_string(), message);
   1751                 assert!(failure.source().is_none());
   1752                 assert!(format!("{failure:?}").contains(&format!("{failure:?}")));
   1753                 assert_eq!(require_store_condition(true, failure), Ok(()));
   1754                 assert_eq!(require_store_condition(false, failure), Err(failure));
   1755                 for kind in [
   1756                     ServiceSqliteErrorKind::Restore,
   1757                     ServiceSqliteErrorKind::Recovery,
   1758                 ] {
   1759                     let error = operation_store(kind, failure);
   1760                     assert_eq!(error.kind(), kind);
   1761                     assert!(error.source().is_some());
   1762                 }
   1763             }
   1764         }
   1765     }
   1766 }
   1767 
   1768 #[cfg(any(target_os = "linux", target_os = "macos"))]
   1769 pub(crate) use store::RestoreMarkerBinding;
   1770 #[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
   1771 pub(crate) use store::TestStoreFailure;
   1772 
   1773 #[cfg(test)]
   1774 mod tests {
   1775 
   1776     use super::*;
   1777     use radroots_runtime_paths::{
   1778         InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver,
   1779         RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId,
   1780     };
   1781 
   1782     fn paths(root: &Path) -> ServiceSqlitePaths {
   1783         paths_for(root, "myc", "primary")
   1784     }
   1785 
   1786     #[test]
   1787     fn marker_contract_failure_inventory_is_complete_and_source_free() {
   1788         for (kind, message) in [
   1789             (
   1790                 RestoreMarkerContractError::MarkerTooLarge,
   1791                 "restore marker exceeds its byte limit",
   1792             ),
   1793             (
   1794                 RestoreMarkerContractError::MalformedEncoding,
   1795                 "restore marker encoding is malformed",
   1796             ),
   1797             (
   1798                 RestoreMarkerContractError::NonCanonicalEncoding,
   1799                 "restore marker encoding is not canonical",
   1800             ),
   1801             (
   1802                 RestoreMarkerContractError::EncodingFailure,
   1803                 "restore marker could not be encoded",
   1804             ),
   1805             (
   1806                 RestoreMarkerContractError::UnsupportedValue,
   1807                 "restore marker schema or value is unsupported",
   1808             ),
   1809             (
   1810                 RestoreMarkerContractError::ChecksumMismatch,
   1811                 "restore marker checksum does not match",
   1812             ),
   1813             (
   1814                 RestoreMarkerContractError::InvalidIdentity,
   1815                 "restore marker identity is invalid",
   1816             ),
   1817             (
   1818                 RestoreMarkerContractError::InvalidLayout,
   1819                 "restore recovery layout is invalid",
   1820             ),
   1821             (
   1822                 RestoreMarkerContractError::IllegalTransition,
   1823                 "restore marker transition is illegal",
   1824             ),
   1825         ] {
   1826             assert_eq!(kind.to_string(), message);
   1827             assert!(kind.source().is_none());
   1828             assert!(format!("{kind:?}").contains(&format!("{kind:?}")));
   1829         }
   1830     }
   1831 
   1832     fn paths_for(root: &Path, service: &str, instance: &str) -> ServiceSqlitePaths {
   1833         let context = RuntimeContext::resolve(
   1834             &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
   1835             RuntimeContextBootstrap::new(
   1836                 RadrootsPathProfile::RepoLocal,
   1837                 Some(root.to_path_buf()),
   1838                 RuntimeContextSource::BootstrapCli,
   1839                 RuntimeContextSource::BootstrapCli,
   1840             )
   1841             .expect("bootstrap"),
   1842             ServiceId::new(service).expect("service"),
   1843             InstanceId::new(instance).expect("instance"),
   1844         )
   1845         .expect("context");
   1846         ServiceSqlitePaths::from_runtime_context(&context).expect("paths")
   1847     }
   1848 
   1849     fn marker(paths: &ServiceSqlitePaths) -> RestoreRecoveryMarker {
   1850         let metadata = ServiceDatabaseMetadata::new(
   1851             paths,
   1852             SourceGeneration::new([7; 32]).expect("generation"),
   1853             NonZeroU32::new(3).expect("schema"),
   1854             1_800_000_000_000,
   1855             ServiceSqliteApplicationId::new(0x5244_5254).expect("application"),
   1856         )
   1857         .expect("metadata");
   1858         RestoreRecoveryMarker::prepared(
   1859             &metadata,
   1860             BackupManifestSha256::from_bytes([8; 32]),
   1861             RestoreArtifactExpectation::new(1, 2, 4096, [3; 32]).expect("live"),
   1862             RestoreArtifactExpectation::new(1, 4, 4096, [5; 32]).expect("staged"),
   1863         )
   1864         .expect("marker")
   1865     }
   1866 
   1867     #[test]
   1868     fn canonical_vector_and_checksum_are_frozen() {
   1869         let root = tempfile::tempdir().expect("root");
   1870         let marker = marker(&paths(root.path()));
   1871         let text = std::str::from_utf8(marker.canonical_bytes()).expect("UTF-8");
   1872         assert_eq!(marker.canonical_bytes().len(), 820);
   1873         assert!(text.starts_with("{\"schema\":\"radroots.service-sqlite.restore-marker\",\"schema_version\":1,\"phase\":\"prepared\""));
   1874         assert!(text.ends_with("\"marker_sha256\":\"026e975f22d45df3b4f46d4d3958e82e3755f7cd2f17d742714e2b55bf381d0f\"}"));
   1875         assert_eq!(
   1876             RestoreRecoveryMarker::from_canonical_bytes(marker.canonical_bytes()).expect("parse"),
   1877             marker
   1878         );
   1879     }
   1880 
   1881     #[test]
   1882     fn prepared_marker_uses_actual_backup_schema_not_a_binary_ceiling() {
   1883         let root = tempfile::tempdir().expect("root");
   1884         let paths = paths(root.path());
   1885         let actual = ServiceDatabaseMetadata::new(
   1886             &paths,
   1887             SourceGeneration::new([7; 32]).expect("generation"),
   1888             NonZeroU32::new(1).expect("actual schema"),
   1889             1_800_000_000_000,
   1890             ServiceSqliteApplicationId::new(0x5244_5254).expect("application"),
   1891         )
   1892         .expect("metadata");
   1893         let marker = RestoreRecoveryMarker::prepared(
   1894             &actual,
   1895             BackupManifestSha256::from_bytes([8; 32]),
   1896             RestoreArtifactExpectation::new(1, 2, 4096, [3; 32]).expect("live"),
   1897             RestoreArtifactExpectation::new(1, 4, 4096, [5; 32]).expect("staged"),
   1898         )
   1899         .expect("marker");
   1900         let text = std::str::from_utf8(marker.canonical_bytes()).expect("text");
   1901         assert!(text.contains("\"state_schema_version\":1"));
   1902         assert!(!text.contains("\"state_schema_version\":3"));
   1903     }
   1904 
   1905     #[test]
   1906     fn marker_identity_matching_binds_each_dimension() {
   1907         let root = tempfile::tempdir().expect("root");
   1908         let paths = paths(root.path());
   1909         let marker = marker(&paths);
   1910         let exact = ServiceDatabaseIdentity::new(
   1911             &paths,
   1912             SourceGeneration::new([7; 32]).expect("generation"),
   1913             NonZeroU32::new(3).expect("schema"),
   1914             ServiceSqliteApplicationId::new(0x5244_5254).expect("application"),
   1915         );
   1916         assert!(marker.matches_identity(&exact));
   1917         let other_service_paths = paths_for(root.path(), "rhi", "primary");
   1918         assert!(!marker.matches_identity(&ServiceDatabaseIdentity::new(
   1919             &other_service_paths,
   1920             exact.source_generation(),
   1921             exact.supported_state_schema_version(),
   1922             exact.application_id(),
   1923         )));
   1924         let other_instance_paths = paths_for(root.path(), "myc", "secondary");
   1925         assert!(!marker.matches_identity(&ServiceDatabaseIdentity::new(
   1926             &other_instance_paths,
   1927             exact.source_generation(),
   1928             exact.supported_state_schema_version(),
   1929             exact.application_id(),
   1930         )));
   1931         assert!(!marker.matches_identity(&ServiceDatabaseIdentity::new(
   1932             &paths,
   1933             SourceGeneration::new([9; 32]).expect("generation"),
   1934             exact.supported_state_schema_version(),
   1935             exact.application_id(),
   1936         )));
   1937         assert!(!marker.matches_identity(&ServiceDatabaseIdentity::new(
   1938             &paths,
   1939             exact.source_generation(),
   1940             exact.supported_state_schema_version(),
   1941             ServiceSqliteApplicationId::new(7).expect("application"),
   1942         )));
   1943         assert!(!marker.matches_identity(&ServiceDatabaseIdentity::new(
   1944             &paths,
   1945             exact.source_generation(),
   1946             NonZeroU32::new(2).expect("schema ceiling"),
   1947             exact.application_id(),
   1948         )));
   1949     }
   1950 
   1951     #[test]
   1952     fn marker_existing_intent_discovers_generation_but_binds_other_dimensions() {
   1953         let root = tempfile::tempdir().expect("root");
   1954         let paths = paths(root.path());
   1955         let marker = marker(&paths);
   1956         let exact = ExistingServiceDatabaseIntent::new(
   1957             &paths,
   1958             NonZeroU32::new(3).expect("schema ceiling"),
   1959             ServiceSqliteApplicationId::new(0x5244_5254).expect("application"),
   1960         );
   1961         assert!(marker.matches_existing_intent(&exact));
   1962 
   1963         let other_service_paths = paths_for(root.path(), "rhi", "primary");
   1964         assert!(
   1965             !marker.matches_existing_intent(&ExistingServiceDatabaseIntent::new(
   1966                 &other_service_paths,
   1967                 exact.supported_state_schema_version(),
   1968                 exact.application_id(),
   1969             ))
   1970         );
   1971         let other_instance_paths = paths_for(root.path(), "myc", "secondary");
   1972         assert!(
   1973             !marker.matches_existing_intent(&ExistingServiceDatabaseIntent::new(
   1974                 &other_instance_paths,
   1975                 exact.supported_state_schema_version(),
   1976                 exact.application_id(),
   1977             ))
   1978         );
   1979         assert!(
   1980             !marker.matches_existing_intent(&ExistingServiceDatabaseIntent::new(
   1981                 &paths,
   1982                 NonZeroU32::new(2).expect("older schema ceiling"),
   1983                 exact.application_id(),
   1984             ))
   1985         );
   1986         assert!(
   1987             !marker.matches_existing_intent(&ExistingServiceDatabaseIntent::new(
   1988                 &paths,
   1989                 exact.supported_state_schema_version(),
   1990                 ServiceSqliteApplicationId::new(7).expect("other application"),
   1991             ))
   1992         );
   1993     }
   1994 
   1995     #[test]
   1996     fn all_phase_edges_and_idempotent_bytes_are_exact() {
   1997         let root = tempfile::tempdir().expect("root");
   1998         let prepared = marker(&paths(root.path()));
   1999         let retained = prepared
   2000             .transitioned_to(RestoreRecoveryPhase::LiveRetained)
   2001             .expect("advance");
   2002         let installed = retained
   2003             .transitioned_to(RestoreRecoveryPhase::ReplacementInstalled)
   2004             .expect("advance");
   2005         let phases = [prepared, retained, installed];
   2006         for current in &phases {
   2007             for next in [
   2008                 RestoreRecoveryPhase::Prepared,
   2009                 RestoreRecoveryPhase::LiveRetained,
   2010                 RestoreRecoveryPhase::ReplacementInstalled,
   2011             ] {
   2012                 let result = current.transitioned_to(next);
   2013                 let allowed = current.phase() == next
   2014                     || matches!(
   2015                         (current.phase(), next),
   2016                         (
   2017                             RestoreRecoveryPhase::Prepared,
   2018                             RestoreRecoveryPhase::LiveRetained
   2019                         ) | (
   2020                             RestoreRecoveryPhase::LiveRetained,
   2021                             RestoreRecoveryPhase::ReplacementInstalled
   2022                         )
   2023                     );
   2024                 assert_eq!(
   2025                     result.is_ok(),
   2026                     allowed,
   2027                     "edge {:?}->{next:?}",
   2028                     current.phase()
   2029                 );
   2030                 if current.phase() == next {
   2031                     assert_eq!(
   2032                         result.expect("same").canonical_bytes(),
   2033                         current.canonical_bytes()
   2034                     );
   2035                 }
   2036             }
   2037         }
   2038     }
   2039 
   2040     #[test]
   2041     fn strict_codec_rejects_tamper_and_noncanonical_inputs() {
   2042         let root = tempfile::tempdir().expect("root");
   2043         let marker = marker(&paths(root.path()));
   2044         let text = std::str::from_utf8(marker.canonical_bytes()).expect("text");
   2045         assert_eq!(
   2046             RestoreRecoveryMarker::from_canonical_bytes(b""),
   2047             Err(RestoreMarkerContractError::MalformedEncoding)
   2048         );
   2049         for altered in [
   2050             format!(" {text}"),
   2051             text.replace(
   2052                 "\"schema\":\"radroots.service-sqlite.restore-marker\"",
   2053                 "\"schema\":\"other\"",
   2054             ),
   2055             text.replace("\"schema_version\":1", "\"schema_version\":2"),
   2056             text.replace("\"phase\":\"prepared\"", "\"phase\":\"unknown\""),
   2057             text.replace("\"phase\":\"prepared\"", "\"phase\":null"),
   2058             text.replace(
   2059                 "\"service\":\"myc\"",
   2060                 "\"service\":\"myc\",\"service\":\"myc\"",
   2061             ),
   2062             text.replace(
   2063                 "\"schema_version\":1,\"phase\"",
   2064                 "\"unknown\":1,\"schema_version\":1,\"phase\"",
   2065             ),
   2066             text.replace("\"device\":1,\"inode\":2", "\"inode\":2,\"device\":1"),
   2067             text.replace("\"source_generation\":\"07", "\"source_generation\":\"0"),
   2068             text.replace("\"source_generation\":\"07", "\"source_generation\":\"A7"),
   2069             text.replace("\"state_schema_version\":3", "\"state_schema_version\":0"),
   2070             text.replace("\"application_id\":1380209236", "\"application_id\":0"),
   2071             text.replace("\"byte_length\":4096", "\"byte_length\":0"),
   2072             text.replace("\"marker_sha256\":\"0", "\"marker_sha256\":\"A"),
   2073         ] {
   2074             assert!(
   2075                 RestoreRecoveryMarker::from_canonical_bytes(altered.as_bytes()).is_err(),
   2076                 "accepted {altered}"
   2077             );
   2078         }
   2079         assert_eq!(
   2080             RestoreRecoveryMarker::from_canonical_bytes(&vec![b'x'; RESTORE_MARKER_MAX_BYTES + 1]),
   2081             Err(RestoreMarkerContractError::MarkerTooLarge)
   2082         );
   2083         assert_ne!(
   2084             RestoreRecoveryMarker::from_canonical_bytes(&vec![b'x'; RESTORE_MARKER_MAX_BYTES]),
   2085             Err(RestoreMarkerContractError::MarkerTooLarge)
   2086         );
   2087     }
   2088 
   2089     #[test]
   2090     fn identity_bounds_layout_and_debug_are_closed() {
   2091         assert!(RestoreArtifactExpectation::new(0, 0, 1, [0; 32]).is_ok());
   2092         assert!(RestoreArtifactExpectation::new(u64::MAX, u64::MAX, 1, [0; 32]).is_ok());
   2093         assert!(RestoreArtifactExpectation::new(1, 1, 0, [0; 32]).is_err());
   2094         assert!(RestoreArtifactExpectation::new(1, 1, i64::MAX as u64 + 1, [0; 32]).is_err());
   2095         let root = tempfile::tempdir().expect("root");
   2096         let paths = paths(root.path());
   2097         let layout = RestoreRecoveryLayout::for_paths(&paths).expect("layout");
   2098         assert_eq!(
   2099             layout.file_names(),
   2100             [
   2101                 LIVE_FILE_NAME,
   2102                 STAGED_FILE_NAME,
   2103                 BACKUP_FILE_NAME,
   2104                 MARKER_FILE_NAME,
   2105                 MARKER_NEXT_FILE_NAME
   2106             ]
   2107         );
   2108         assert!(layout.live.starts_with(&layout.state_directory));
   2109         assert!(layout.staged.starts_with(&layout.state_directory));
   2110         assert!(layout.backup.starts_with(&layout.state_directory));
   2111         assert_eq!(format!("{layout:?}"), "RestoreRecoveryLayout([redacted])");
   2112         assert_eq!(
   2113             format!("{:?}", marker(&paths)),
   2114             "RestoreRecoveryMarker { schema_version: 1, phase: Prepared, .. }"
   2115         );
   2116     }
   2117 
   2118     #[cfg(any(target_os = "linux", target_os = "macos"))]
   2119     #[test]
   2120     fn durable_store_creates_loads_and_advances_owner_only_marker() {
   2121         use std::{fs, os::unix::fs::PermissionsExt};
   2122         let root = tempfile::tempdir().expect("root");
   2123         let paths = paths(root.path());
   2124         fs::create_dir_all(paths.state_database().parent().expect("parent")).expect("state dir");
   2125         fs::set_permissions(
   2126             paths.state_database().parent().expect("parent"),
   2127             fs::Permissions::from_mode(0o700),
   2128         )
   2129         .expect("mode");
   2130         let authority = WriterAuthority::acquire(&paths, crate::OpenMode::Initialize)
   2131             .expect("authority")
   2132             .expect("writer");
   2133         let prepared = marker(&paths);
   2134         let binding = RestoreMarkerBinding::create(&paths, &authority, &prepared).expect("create");
   2135         assert_eq!(
   2136             fs::metadata(paths.state_database().with_file_name(MARKER_FILE_NAME))
   2137                 .expect("metadata")
   2138                 .permissions()
   2139                 .mode()
   2140                 & 0o777,
   2141             0o600
   2142         );
   2143         binding.validate(&paths).expect("validate");
   2144         drop(binding);
   2145         let loaded = RestoreMarkerBinding::load(&paths)
   2146             .expect("load")
   2147             .expect("present");
   2148         assert_eq!(loaded.marker().phase(), RestoreRecoveryPhase::Prepared);
   2149         let loaded = loaded
   2150             .advance(&paths, &authority, RestoreRecoveryPhase::LiveRetained)
   2151             .expect("advance");
   2152         assert_eq!(loaded.marker().phase(), RestoreRecoveryPhase::LiveRetained);
   2153         let same = loaded
   2154             .advance(&paths, &authority, RestoreRecoveryPhase::LiveRetained)
   2155             .expect("same");
   2156         assert_eq!(same.marker().phase(), RestoreRecoveryPhase::LiveRetained);
   2157         assert!(
   2158             !paths
   2159                 .state_database()
   2160                 .with_file_name(MARKER_NEXT_FILE_NAME)
   2161                 .exists()
   2162         );
   2163     }
   2164 
   2165     #[cfg(any(target_os = "linux", target_os = "macos"))]
   2166     #[test]
   2167     fn atomic_advance_storage_full_failures_leave_exact_old_or_new_valid_marker() {
   2168         use super::store::TestStoreFailure;
   2169         use std::{fs, os::unix::fs::PermissionsExt};
   2170 
   2171         for (instance, failure, expected_phase) in [
   2172             (
   2173                 "pre-rename",
   2174                 TestStoreFailure::ScratchSync,
   2175                 RestoreRecoveryPhase::Prepared,
   2176             ),
   2177             (
   2178                 "post-rename",
   2179                 TestStoreFailure::ParentSyncAfterRename,
   2180                 RestoreRecoveryPhase::LiveRetained,
   2181             ),
   2182         ] {
   2183             let root = tempfile::tempdir().expect("root");
   2184             let paths = paths(&root.path().join(instance));
   2185             let parent = paths.state_database().parent().expect("parent");
   2186             fs::create_dir_all(parent).expect("state dir");
   2187             fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).expect("mode");
   2188             let authority = WriterAuthority::acquire(&paths, crate::OpenMode::Initialize)
   2189                 .expect("authority")
   2190                 .expect("writer");
   2191             let binding =
   2192                 RestoreMarkerBinding::create(&paths, &authority, &marker(&paths)).expect("create");
   2193             assert_eq!(
   2194                 binding
   2195                     .test_advance_with_failure(
   2196                         &paths,
   2197                         &authority,
   2198                         RestoreRecoveryPhase::LiveRetained,
   2199                         failure,
   2200                     )
   2201                     .expect_err("injected failure")
   2202                     .kind(),
   2203                 ServiceSqliteErrorKind::Restore
   2204             );
   2205             let recovered = RestoreMarkerBinding::load(&paths)
   2206                 .expect("read valid durable state")
   2207                 .expect("marker remains");
   2208             assert_eq!(recovered.marker().phase(), expected_phase);
   2209             assert!(
   2210                 !paths
   2211                     .state_database()
   2212                     .with_file_name(MARKER_NEXT_FILE_NAME)
   2213                     .exists()
   2214             );
   2215         }
   2216     }
   2217 
   2218     #[cfg(any(target_os = "linux", target_os = "macos"))]
   2219     #[test]
   2220     fn initial_store_requires_prepared_and_authority_wins_combined_failure() {
   2221         use super::store::TestStoreFailure;
   2222         use std::{fs, os::unix::fs::PermissionsExt};
   2223 
   2224         for phase in [
   2225             RestoreRecoveryPhase::LiveRetained,
   2226             RestoreRecoveryPhase::ReplacementInstalled,
   2227         ] {
   2228             let root = tempfile::tempdir().expect("root");
   2229             let paths = paths(root.path());
   2230             let parent = paths.state_database().parent().expect("parent");
   2231             fs::create_dir_all(parent).expect("state dir");
   2232             fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).expect("mode");
   2233             let authority = WriterAuthority::acquire(&paths, crate::OpenMode::Initialize)
   2234                 .expect("authority")
   2235                 .expect("writer");
   2236             let later = marker(&paths)
   2237                 .transitioned_to(RestoreRecoveryPhase::LiveRetained)
   2238                 .expect("retained");
   2239             let later = if phase == RestoreRecoveryPhase::ReplacementInstalled {
   2240                 later.transitioned_to(phase).expect("installed")
   2241             } else {
   2242                 later
   2243             };
   2244             assert_eq!(
   2245                 RestoreMarkerBinding::create(&paths, &authority, &later)
   2246                     .expect_err("initial later phase")
   2247                     .kind(),
   2248                 ServiceSqliteErrorKind::Restore
   2249             );
   2250             assert!(!parent.join(MARKER_FILE_NAME).exists());
   2251         }
   2252 
   2253         let root = tempfile::tempdir().expect("root");
   2254         let paths = paths(root.path());
   2255         let parent = paths.state_database().parent().expect("parent");
   2256         fs::create_dir_all(parent).expect("state dir");
   2257         fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).expect("mode");
   2258         let authority = WriterAuthority::acquire(&paths, crate::OpenMode::Initialize)
   2259             .expect("authority")
   2260             .expect("writer");
   2261         let binding =
   2262             RestoreMarkerBinding::create(&paths, &authority, &marker(&paths)).expect("create");
   2263         assert_eq!(
   2264             binding
   2265                 .test_advance_with_failure(
   2266                     &paths,
   2267                     &authority,
   2268                     RestoreRecoveryPhase::LiveRetained,
   2269                     TestStoreFailure::AuthorityDriftAndScratchSync,
   2270                 )
   2271                 .expect_err("authority drift")
   2272                 .kind(),
   2273             ServiceSqliteErrorKind::Authority
   2274         );
   2275         assert!(parent.join(MARKER_NEXT_FILE_NAME).exists());
   2276     }
   2277 
   2278     #[cfg(any(target_os = "linux", target_os = "macos"))]
   2279     #[test]
   2280     fn active_marker_conflict_is_restore_and_directory_replacement_is_authority() {
   2281         use std::{fs, os::unix::fs::PermissionsExt};
   2282 
   2283         let root = tempfile::tempdir().expect("root");
   2284         let marker_paths = paths(&root.path().join("marker-conflict"));
   2285         let parent = marker_paths.state_database().parent().expect("parent");
   2286         fs::create_dir_all(parent).expect("state dir");
   2287         fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).expect("mode");
   2288         let authority = WriterAuthority::acquire(&marker_paths, crate::OpenMode::Initialize)
   2289             .expect("authority")
   2290             .expect("writer");
   2291         let binding =
   2292             RestoreMarkerBinding::create(&marker_paths, &authority, &marker(&marker_paths))
   2293                 .expect("create");
   2294         let marker_path = parent.join(MARKER_FILE_NAME);
   2295         let replacement_bytes = fs::read(&marker_path).expect("marker bytes");
   2296         fs::rename(&marker_path, parent.join("retained-marker")).expect("retain marker");
   2297         fs::write(&marker_path, replacement_bytes).expect("replacement marker");
   2298         fs::set_permissions(&marker_path, fs::Permissions::from_mode(0o600)).expect("mode");
   2299         assert_eq!(
   2300             binding
   2301                 .advance(
   2302                     &marker_paths,
   2303                     &authority,
   2304                     RestoreRecoveryPhase::LiveRetained,
   2305                 )
   2306                 .expect_err("marker replacement")
   2307                 .kind(),
   2308             ServiceSqliteErrorKind::Restore
   2309         );
   2310 
   2311         let root = tempfile::tempdir().expect("root");
   2312         let paths = paths(&root.path().join("directory-conflict"));
   2313         let parent = paths
   2314             .state_database()
   2315             .parent()
   2316             .expect("parent")
   2317             .to_path_buf();
   2318         fs::create_dir_all(&parent).expect("state dir");
   2319         fs::set_permissions(&parent, fs::Permissions::from_mode(0o700)).expect("mode");
   2320         let authority = WriterAuthority::acquire(&paths, crate::OpenMode::Initialize)
   2321             .expect("authority")
   2322             .expect("writer");
   2323         let binding =
   2324             RestoreMarkerBinding::create(&paths, &authority, &marker(&paths)).expect("create");
   2325         let retained_parent = parent.with_file_name("state-retained");
   2326         fs::rename(&parent, &retained_parent).expect("retain directory");
   2327         fs::create_dir(&parent).expect("replacement directory");
   2328         fs::set_permissions(&parent, fs::Permissions::from_mode(0o700)).expect("mode");
   2329         assert_eq!(
   2330             binding
   2331                 .advance(&paths, &authority, RestoreRecoveryPhase::LiveRetained,)
   2332                 .expect_err("directory replacement")
   2333                 .kind(),
   2334             ServiceSqliteErrorKind::Authority
   2335         );
   2336     }
   2337 
   2338     #[cfg(any(target_os = "linux", target_os = "macos"))]
   2339     #[test]
   2340     fn load_rejects_stale_scratch_hardlinks_and_wrong_mode() {
   2341         use std::{fs, os::unix::fs::PermissionsExt};
   2342 
   2343         for shape in ["stale-next", "hardlink", "wrong-mode"] {
   2344             let root = tempfile::tempdir().expect("root");
   2345             let paths = paths(root.path());
   2346             let parent = paths.state_database().parent().expect("parent");
   2347             fs::create_dir_all(parent).expect("state dir");
   2348             fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).expect("mode");
   2349             let authority = WriterAuthority::acquire(&paths, crate::OpenMode::Initialize)
   2350                 .expect("authority")
   2351                 .expect("writer");
   2352             drop(
   2353                 RestoreMarkerBinding::create(&paths, &authority, &marker(&paths)).expect("create"),
   2354             );
   2355             let marker_path = parent.join(MARKER_FILE_NAME);
   2356             match shape {
   2357                 "stale-next" => {
   2358                     fs::write(parent.join(MARKER_NEXT_FILE_NAME), b"evidence").expect("stale next")
   2359                 }
   2360                 "hardlink" => {
   2361                     fs::hard_link(&marker_path, parent.join("retained-link")).expect("hard link");
   2362                 }
   2363                 "wrong-mode" => {
   2364                     fs::set_permissions(&marker_path, fs::Permissions::from_mode(0o640))
   2365                         .expect("mode");
   2366                 }
   2367                 _ => unreachable!(),
   2368             }
   2369             assert_eq!(
   2370                 RestoreMarkerBinding::load(&paths)
   2371                     .expect_err("invalid artifact")
   2372                     .kind(),
   2373                 ServiceSqliteErrorKind::Recovery
   2374             );
   2375         }
   2376     }
   2377 
   2378     #[cfg(any(target_os = "linux", target_os = "macos"))]
   2379     #[test]
   2380     fn store_rejects_collisions_shapes_tamper_and_insecure_parent() {
   2381         use std::{
   2382             fs,
   2383             os::unix::fs::{PermissionsExt, symlink},
   2384         };
   2385         for shape in ["file", "directory", "symlink"] {
   2386             let root = tempfile::tempdir().expect("root");
   2387             let paths = paths(root.path());
   2388             let parent = paths.state_database().parent().expect("parent");
   2389             fs::create_dir_all(parent).expect("parent");
   2390             fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).expect("mode");
   2391             let marker_path = parent.join(MARKER_FILE_NAME);
   2392             match shape {
   2393                 "file" => fs::write(&marker_path, b"collision").expect("file"),
   2394                 "directory" => fs::create_dir(&marker_path).expect("directory"),
   2395                 "symlink" => symlink(parent.join("missing"), &marker_path).expect("symlink"),
   2396                 _ => unreachable!(),
   2397             }
   2398             let authority = WriterAuthority::acquire(&paths, crate::OpenMode::Initialize)
   2399                 .expect("authority")
   2400                 .expect("writer");
   2401             assert_eq!(
   2402                 RestoreMarkerBinding::create(&paths, &authority, &marker(&paths))
   2403                     .expect_err("collision")
   2404                     .kind(),
   2405                 ServiceSqliteErrorKind::Restore
   2406             );
   2407         }
   2408 
   2409         let root = tempfile::tempdir().expect("root");
   2410         let paths = paths(root.path());
   2411         let parent = paths.state_database().parent().expect("parent");
   2412         fs::create_dir_all(parent).expect("parent");
   2413         fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).expect("mode");
   2414         let authority = WriterAuthority::acquire(&paths, crate::OpenMode::Initialize)
   2415             .expect("authority")
   2416             .expect("writer");
   2417         drop(RestoreMarkerBinding::create(&paths, &authority, &marker(&paths)).expect("create"));
   2418         fs::write(parent.join(MARKER_FILE_NAME), b"tampered").expect("tamper");
   2419         assert_eq!(
   2420             RestoreMarkerBinding::load(&paths)
   2421                 .expect_err("tamper")
   2422                 .kind(),
   2423             ServiceSqliteErrorKind::Recovery
   2424         );
   2425         fs::remove_file(parent.join(MARKER_FILE_NAME)).expect("remove");
   2426         fs::set_permissions(parent, fs::Permissions::from_mode(0o722)).expect("insecure");
   2427         assert_eq!(
   2428             RestoreMarkerBinding::load(&paths)
   2429                 .expect_err("directory")
   2430                 .kind(),
   2431             ServiceSqliteErrorKind::Recovery
   2432         );
   2433     }
   2434 
   2435     #[test]
   2436     fn marker_errors_and_debug_are_path_content_and_digest_free() {
   2437         let root = tempfile::tempdir().expect("secret-root");
   2438         let paths = paths(root.path());
   2439         let redacted_marker = marker(&paths);
   2440         let debug = format!("{redacted_marker:?}");
   2441         for sensitive in [
   2442             "secret-root",
   2443             "state.sqlite",
   2444             "myc",
   2445             "primary",
   2446             "07070707",
   2447             "08080808",
   2448         ] {
   2449             assert!(!debug.contains(sensitive));
   2450         }
   2451         for error in [
   2452             RestoreMarkerContractError::MarkerTooLarge,
   2453             RestoreMarkerContractError::MalformedEncoding,
   2454             RestoreMarkerContractError::NonCanonicalEncoding,
   2455             RestoreMarkerContractError::EncodingFailure,
   2456             RestoreMarkerContractError::UnsupportedValue,
   2457             RestoreMarkerContractError::ChecksumMismatch,
   2458             RestoreMarkerContractError::InvalidIdentity,
   2459             RestoreMarkerContractError::InvalidLayout,
   2460             RestoreMarkerContractError::IllegalTransition,
   2461         ] {
   2462             let rendered = format!("{error:?} {error}");
   2463             assert!(rendered.is_ascii());
   2464             assert!(!rendered.contains('/'));
   2465             assert!(!rendered.contains(".sqlite"));
   2466         }
   2467         for error in [
   2468             RestoreMarkerContractError::MarkerTooLarge,
   2469             RestoreMarkerContractError::MalformedEncoding,
   2470             RestoreMarkerContractError::NonCanonicalEncoding,
   2471             RestoreMarkerContractError::EncodingFailure,
   2472             RestoreMarkerContractError::UnsupportedValue,
   2473             RestoreMarkerContractError::ChecksumMismatch,
   2474             RestoreMarkerContractError::InvalidIdentity,
   2475             RestoreMarkerContractError::InvalidLayout,
   2476             RestoreMarkerContractError::IllegalTransition,
   2477         ] {
   2478             assert_eq!(require_marker_contract(true, error), Ok(()));
   2479             assert_eq!(require_marker_contract(false, error), Err(error));
   2480         }
   2481 
   2482         let layout = RestoreRecoveryLayout::for_paths(&paths).expect("layout");
   2483         assert!(layout_uses_fixed_marker_name(&layout));
   2484         let mut invalid_layout = layout;
   2485         invalid_layout.marker = invalid_layout.marker.with_file_name("other-marker");
   2486         assert!(!layout_uses_fixed_marker_name(&invalid_layout));
   2487 
   2488         let prepared = marker(&paths);
   2489         let retained = prepared
   2490             .transitioned_to(RestoreRecoveryPhase::LiveRetained)
   2491             .expect("retained");
   2492         let installed = retained
   2493             .transitioned_to(RestoreRecoveryPhase::ReplacementInstalled)
   2494             .expect("installed");
   2495         assert!(interrupted_successor_matches(&prepared, &retained));
   2496         assert!(interrupted_successor_matches(&retained, &installed));
   2497         assert!(!interrupted_successor_matches(&prepared, &prepared));
   2498         assert!(!interrupted_successor_matches(&prepared, &installed));
   2499         let other_prepared = marker(&paths);
   2500         assert!(!interrupted_successor_matches(&retained, &other_prepared));
   2501     }
   2502 }