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(¤t)? == 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(¤t_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(¤t_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(¤t_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(¤t_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(¤t)? == 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 }