authority.rs (24564B)
1 //! Lifetime authority for the sole writable service database owner. 2 3 use core::fmt; 4 use std::{error::Error, fs::File}; 5 6 #[cfg(any(target_os = "linux", target_os = "macos"))] 7 use std::path::PathBuf; 8 9 use fs2::FileExt; 10 11 use crate::{OpenMode, ServiceSqliteError, ServiceSqliteErrorKind, ServiceSqlitePaths}; 12 13 /// Exclusive lifetime capability required by writable SQLite open modes. 14 /// 15 /// This capability is deliberately non-cloneable and exposes no descriptor or 16 /// lock path: 17 /// 18 /// ```compile_fail 19 /// use radroots_service_sqlite::WriterAuthority; 20 /// 21 /// fn require_clone<T: Clone>() {} 22 /// require_clone::<WriterAuthority>(); 23 /// ``` 24 pub struct WriterAuthority { 25 file: Option<File>, 26 #[cfg(any(target_os = "linux", target_os = "macos"))] 27 database_path: PathBuf, 28 #[cfg(any(target_os = "linux", target_os = "macos"))] 29 directory: File, 30 #[cfg(any(target_os = "linux", target_os = "macos"))] 31 directory_device: u64, 32 #[cfg(any(target_os = "linux", target_os = "macos"))] 33 directory_inode: u64, 34 #[cfg(any(target_os = "linux", target_os = "macos"))] 35 lock_device: u64, 36 #[cfg(any(target_os = "linux", target_os = "macos"))] 37 lock_inode: u64, 38 } 39 40 impl WriterAuthority { 41 /// Acquires writer authority without waiting or touching database state. 42 /// 43 /// Read-only inspection requires no writer authority and performs no 44 /// filesystem operation. 45 pub fn acquire( 46 paths: &ServiceSqlitePaths, 47 mode: OpenMode, 48 ) -> Result<Option<Self>, ServiceSqliteError> { 49 if !mode.requires_writer_authority() { 50 return Ok(None); 51 } 52 acquire_supported(paths).map(Some).map_err(authority_error) 53 } 54 55 /// Returns whether this capability still holds the advisory lock. 56 #[must_use] 57 pub fn is_held(&self) -> bool { 58 self.file.is_some() 59 } 60 61 #[cfg(any(target_os = "linux", target_os = "macos"))] 62 pub(crate) fn directory(&self) -> &File { 63 &self.directory 64 } 65 66 #[cfg(any(target_os = "linux", target_os = "macos"))] 67 pub(crate) fn validate_for( 68 &self, 69 paths: &ServiceSqlitePaths, 70 ) -> Result<(), ServiceSqliteError> { 71 require_authority_condition( 72 self.is_held() && self.database_path == paths.state_database(), 73 WriterAuthorityCause::Mismatched, 74 ) 75 .map_err(authority_error)?; 76 77 #[cfg(any(target_os = "linux", target_os = "macos"))] 78 validate_authority_binding(self, paths).map_err(authority_error)?; 79 80 Ok(()) 81 } 82 83 /// Explicitly releases writer authority; subsequent calls are no-ops. 84 pub fn release(&mut self) -> Result<(), ServiceSqliteError> { 85 let Some(file) = self.file.as_ref() else { 86 return Ok(()); 87 }; 88 FileExt::unlock(file).map_err(|_| authority_error(WriterAuthorityCause::UnlockFailed))?; 89 self.file.take(); 90 Ok(()) 91 } 92 } 93 94 impl fmt::Debug for WriterAuthority { 95 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 96 formatter 97 .debug_struct("WriterAuthority") 98 .field("state", &if self.is_held() { "held" } else { "released" }) 99 .finish() 100 } 101 } 102 103 impl Drop for WriterAuthority { 104 fn drop(&mut self) { 105 if let Some(file) = self.file.take() { 106 let _ = FileExt::unlock(&file); 107 } 108 } 109 } 110 111 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 112 enum WriterAuthorityCause { 113 #[cfg(not(any(target_os = "linux", target_os = "macos")))] 114 UnsupportedPlatform, 115 #[cfg(any(target_os = "linux", target_os = "macos"))] 116 StateDirectoryUnavailable, 117 #[cfg(any(target_os = "linux", target_os = "macos"))] 118 StateDirectoryInvalidType, 119 #[cfg(any(target_os = "linux", target_os = "macos"))] 120 StateDirectoryWrongOwner, 121 #[cfg(any(target_os = "linux", target_os = "macos"))] 122 StateDirectoryInsecurePermissions, 123 #[cfg(any(target_os = "linux", target_os = "macos"))] 124 LockUnavailable, 125 #[cfg(any(target_os = "linux", target_os = "macos"))] 126 LockInvalidType, 127 #[cfg(any(target_os = "linux", target_os = "macos"))] 128 LockMultipleLinks, 129 #[cfg(any(target_os = "linux", target_os = "macos"))] 130 LockWrongOwner, 131 #[cfg(any(target_os = "linux", target_os = "macos"))] 132 Contended, 133 #[cfg(any(target_os = "linux", target_os = "macos"))] 134 Mismatched, 135 UnlockFailed, 136 } 137 138 impl fmt::Display for WriterAuthorityCause { 139 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 140 formatter.write_str(match self { 141 #[cfg(not(any(target_os = "linux", target_os = "macos")))] 142 Self::UnsupportedPlatform => "SQLite writer authority is unsupported on this platform", 143 #[cfg(any(target_os = "linux", target_os = "macos"))] 144 Self::StateDirectoryUnavailable => "SQLite state directory is unavailable", 145 #[cfg(any(target_os = "linux", target_os = "macos"))] 146 Self::StateDirectoryInvalidType => "SQLite state directory has an invalid type", 147 #[cfg(any(target_os = "linux", target_os = "macos"))] 148 Self::StateDirectoryWrongOwner => "SQLite state directory has the wrong owner", 149 #[cfg(any(target_os = "linux", target_os = "macos"))] 150 Self::StateDirectoryInsecurePermissions => { 151 "SQLite state directory permissions are insecure" 152 } 153 #[cfg(any(target_os = "linux", target_os = "macos"))] 154 Self::LockUnavailable => "SQLite writer lock is unavailable", 155 #[cfg(any(target_os = "linux", target_os = "macos"))] 156 Self::LockInvalidType => "SQLite writer lock has an invalid type", 157 #[cfg(any(target_os = "linux", target_os = "macos"))] 158 Self::LockMultipleLinks => "SQLite writer lock has multiple links", 159 #[cfg(any(target_os = "linux", target_os = "macos"))] 160 Self::LockWrongOwner => "SQLite writer lock has the wrong owner", 161 #[cfg(any(target_os = "linux", target_os = "macos"))] 162 Self::Contended => "another SQLite writer is active", 163 #[cfg(any(target_os = "linux", target_os = "macos"))] 164 Self::Mismatched => "SQLite writer authority does not match this database", 165 Self::UnlockFailed => "SQLite writer authority could not be released", 166 }) 167 } 168 } 169 170 impl Error for WriterAuthorityCause {} 171 172 fn authority_error(cause: WriterAuthorityCause) -> ServiceSqliteError { 173 ServiceSqliteError::with_source(ServiceSqliteErrorKind::Authority, cause) 174 } 175 176 #[cfg(any(target_os = "linux", target_os = "macos"))] 177 fn acquire_supported(paths: &ServiceSqlitePaths) -> Result<WriterAuthority, WriterAuthorityCause> { 178 use rustix::{ 179 fs::{FileType, Mode, OFlags, fchmod, fstat, open, openat}, 180 process::geteuid, 181 }; 182 183 let state_directory = paths 184 .state_lock() 185 .parent() 186 .ok_or(WriterAuthorityCause::StateDirectoryUnavailable)?; 187 let directory = open( 188 state_directory, 189 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC, 190 Mode::empty(), 191 ) 192 .map_err(|_| WriterAuthorityCause::StateDirectoryUnavailable)?; 193 let directory_status = 194 fstat(&directory).map_err(|_| WriterAuthorityCause::StateDirectoryUnavailable)?; 195 validate_directory( 196 FileType::from_raw_mode(directory_status.st_mode).is_dir(), 197 directory_status.st_uid, 198 crate::native_metadata::mode(directory_status.st_mode), 199 geteuid().as_raw(), 200 )?; 201 202 let descriptor = openat( 203 &directory, 204 radroots_runtime_paths::SERVICE_STATE_LOCK_FILE_NAME, 205 OFlags::RDWR | OFlags::CREATE | OFlags::NOFOLLOW | OFlags::CLOEXEC | OFlags::NONBLOCK, 206 Mode::RUSR | Mode::WUSR, 207 ) 208 .map_err(|_| WriterAuthorityCause::LockUnavailable)?; 209 let lock_status = fstat(&descriptor).map_err(|_| WriterAuthorityCause::LockUnavailable)?; 210 validate_lock( 211 FileType::from_raw_mode(lock_status.st_mode).is_file(), 212 crate::native_metadata::link_count(lock_status.st_nlink), 213 lock_status.st_uid, 214 geteuid().as_raw(), 215 )?; 216 fchmod(&descriptor, Mode::RUSR | Mode::WUSR) 217 .map_err(|_| WriterAuthorityCause::LockUnavailable)?; 218 219 let lock_status = fstat(&descriptor).map_err(|_| WriterAuthorityCause::LockUnavailable)?; 220 require_authority_condition( 221 crate::native_metadata::mode(lock_status.st_mode) & 0o777 == 0o600, 222 WriterAuthorityCause::LockUnavailable, 223 )?; 224 let directory_device = crate::native_metadata::device(directory_status.st_dev) 225 .map_err(|_| WriterAuthorityCause::StateDirectoryUnavailable)?; 226 let lock_device = crate::native_metadata::device(lock_status.st_dev) 227 .map_err(|_| WriterAuthorityCause::LockUnavailable)?; 228 let file = File::from(descriptor); 229 let directory = File::from(directory); 230 match FileExt::try_lock_exclusive(&file) { 231 Ok(()) => { 232 let authority = WriterAuthority { 233 file: Some(file), 234 database_path: paths.state_database().to_path_buf(), 235 directory, 236 directory_device, 237 directory_inode: directory_status.st_ino, 238 lock_device, 239 lock_inode: lock_status.st_ino, 240 }; 241 validate_authority_binding(&authority, paths)?; 242 Ok(authority) 243 } 244 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { 245 Err(WriterAuthorityCause::Contended) 246 } 247 Err(_) => Err(WriterAuthorityCause::LockUnavailable), 248 } 249 } 250 251 #[cfg(not(any(target_os = "linux", target_os = "macos")))] 252 fn acquire_supported(_paths: &ServiceSqlitePaths) -> Result<WriterAuthority, WriterAuthorityCause> { 253 Err(WriterAuthorityCause::UnsupportedPlatform) 254 } 255 256 #[cfg(any(target_os = "linux", target_os = "macos"))] 257 fn validate_directory( 258 is_directory: bool, 259 actual_uid: u32, 260 mode: u32, 261 expected_uid: u32, 262 ) -> Result<(), WriterAuthorityCause> { 263 if !is_directory { 264 return Err(WriterAuthorityCause::StateDirectoryInvalidType); 265 } 266 if actual_uid != expected_uid { 267 return Err(WriterAuthorityCause::StateDirectoryWrongOwner); 268 } 269 if mode & 0o022 != 0 { 270 return Err(WriterAuthorityCause::StateDirectoryInsecurePermissions); 271 } 272 Ok(()) 273 } 274 275 #[cfg(any(target_os = "linux", target_os = "macos"))] 276 fn validate_lock( 277 is_regular_file: bool, 278 link_count: u64, 279 actual_uid: u32, 280 expected_uid: u32, 281 ) -> Result<(), WriterAuthorityCause> { 282 if !is_regular_file { 283 return Err(WriterAuthorityCause::LockInvalidType); 284 } 285 if link_count != 1 { 286 return Err(WriterAuthorityCause::LockMultipleLinks); 287 } 288 if actual_uid != expected_uid { 289 return Err(WriterAuthorityCause::LockWrongOwner); 290 } 291 Ok(()) 292 } 293 294 #[cfg(any(target_os = "linux", target_os = "macos"))] 295 fn validate_authority_binding( 296 authority: &WriterAuthority, 297 paths: &ServiceSqlitePaths, 298 ) -> Result<(), WriterAuthorityCause> { 299 use rustix::{ 300 fs::{FileType, Mode, OFlags, fstat, open, openat}, 301 process::geteuid, 302 }; 303 304 let directory_path = paths 305 .state_database() 306 .parent() 307 .filter(|parent| Some(*parent) == paths.state_lock().parent()) 308 .ok_or(WriterAuthorityCause::Mismatched)?; 309 let current_directory = open( 310 directory_path, 311 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC, 312 Mode::empty(), 313 ) 314 .map_err(|_| WriterAuthorityCause::Mismatched)?; 315 let held_directory = 316 fstat(&authority.directory).map_err(|_| WriterAuthorityCause::Mismatched)?; 317 let current_directory_status = 318 fstat(¤t_directory).map_err(|_| WriterAuthorityCause::Mismatched)?; 319 let held_directory_device = crate::native_metadata::device(held_directory.st_dev) 320 .map_err(|_| WriterAuthorityCause::Mismatched)?; 321 let current_directory_device = crate::native_metadata::device(current_directory_status.st_dev) 322 .map_err(|_| WriterAuthorityCause::Mismatched)?; 323 require_authority_condition( 324 crate::all_constraints([ 325 crate::native_metadata::secure_directory( 326 FileType::from_raw_mode(held_directory.st_mode).is_dir(), 327 held_directory.st_uid, 328 geteuid().as_raw(), 329 crate::native_metadata::mode(held_directory.st_mode), 330 ), 331 crate::native_metadata::secure_directory( 332 FileType::from_raw_mode(current_directory_status.st_mode).is_dir(), 333 current_directory_status.st_uid, 334 geteuid().as_raw(), 335 crate::native_metadata::mode(current_directory_status.st_mode), 336 ), 337 crate::native_metadata::identity_pair_matches( 338 held_directory_device, 339 held_directory.st_ino, 340 current_directory_device, 341 current_directory_status.st_ino, 342 authority.directory_device, 343 authority.directory_inode, 344 ), 345 ]), 346 WriterAuthorityCause::Mismatched, 347 )?; 348 349 let current_lock = openat( 350 ¤t_directory, 351 radroots_runtime_paths::SERVICE_STATE_LOCK_FILE_NAME, 352 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC | OFlags::NONBLOCK, 353 Mode::empty(), 354 ) 355 .map_err(|_| WriterAuthorityCause::Mismatched)?; 356 let held_lock = fstat( 357 authority 358 .file 359 .as_ref() 360 .ok_or(WriterAuthorityCause::Mismatched)?, 361 ) 362 .map_err(|_| WriterAuthorityCause::Mismatched)?; 363 let current_lock_status = fstat(¤t_lock).map_err(|_| WriterAuthorityCause::Mismatched)?; 364 let held_lock_device = crate::native_metadata::device(held_lock.st_dev) 365 .map_err(|_| WriterAuthorityCause::Mismatched)?; 366 let current_lock_device = crate::native_metadata::device(current_lock_status.st_dev) 367 .map_err(|_| WriterAuthorityCause::Mismatched)?; 368 require_authority_condition( 369 crate::all_constraints([ 370 crate::native_metadata::exact_regular_file( 371 FileType::from_raw_mode(held_lock.st_mode).is_file(), 372 crate::native_metadata::link_count(held_lock.st_nlink), 373 held_lock.st_uid, 374 geteuid().as_raw(), 375 crate::native_metadata::mode(held_lock.st_mode), 376 ), 377 crate::native_metadata::exact_regular_file( 378 FileType::from_raw_mode(current_lock_status.st_mode).is_file(), 379 crate::native_metadata::link_count(current_lock_status.st_nlink), 380 current_lock_status.st_uid, 381 geteuid().as_raw(), 382 crate::native_metadata::mode(current_lock_status.st_mode), 383 ), 384 crate::native_metadata::identity_pair_matches( 385 held_lock_device, 386 held_lock.st_ino, 387 current_lock_device, 388 current_lock_status.st_ino, 389 authority.lock_device, 390 authority.lock_inode, 391 ), 392 ]), 393 WriterAuthorityCause::Mismatched, 394 )?; 395 Ok(()) 396 } 397 398 #[cfg(any(target_os = "linux", target_os = "macos"))] 399 fn require_authority_condition( 400 condition: bool, 401 cause: WriterAuthorityCause, 402 ) -> Result<(), WriterAuthorityCause> { 403 condition.then_some(()).ok_or(cause) 404 } 405 406 #[cfg(all(test, any(target_os = "linux", target_os = "macos")))] 407 mod tests { 408 use std::{ 409 fs, 410 os::unix::fs::{MetadataExt, PermissionsExt, symlink}, 411 path::Path, 412 }; 413 414 use radroots_runtime_paths::{ 415 InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, 416 RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, ServiceId, 417 }; 418 419 use super::*; 420 421 fn paths(root: &Path, instance: &str) -> ServiceSqlitePaths { 422 let context = RuntimeContext::resolve( 423 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 424 RuntimeContextBootstrap::new( 425 RadrootsPathProfile::RepoLocal, 426 Some(root.to_path_buf()), 427 RuntimeContextSource::BootstrapCli, 428 RuntimeContextSource::BootstrapCli, 429 ) 430 .expect("bootstrap"), 431 ServiceId::new("myc").expect("service"), 432 InstanceId::new(instance).expect("instance"), 433 ) 434 .expect("runtime context"); 435 ServiceSqlitePaths::from_runtime_context(&context).expect("SQLite paths") 436 } 437 438 fn prepare(paths: &ServiceSqlitePaths) { 439 fs::create_dir_all(paths.state_lock().parent().expect("state directory")) 440 .expect("create state directory"); 441 } 442 443 #[test] 444 fn one_writer_excludes_a_second_until_release_or_drop() { 445 let root = tempfile::tempdir().expect("root"); 446 let paths = paths(root.path(), "primary"); 447 prepare(&paths); 448 449 let mut first = WriterAuthority::acquire(&paths, OpenMode::Initialize) 450 .expect("first acquisition") 451 .expect("writer capability"); 452 assert!(first.is_held()); 453 let contended = WriterAuthority::acquire(&paths, OpenMode::ReadWriteExisting) 454 .expect_err("second writer must fail"); 455 assert_eq!(contended.kind(), ServiceSqliteErrorKind::Authority); 456 assert_eq!( 457 contended.source().map(ToString::to_string).as_deref(), 458 Some("another SQLite writer is active") 459 ); 460 461 first.release().expect("release"); 462 assert!(!first.is_held()); 463 assert_eq!( 464 first 465 .validate_for(&paths) 466 .expect_err("released authority cannot validate") 467 .kind(), 468 ServiceSqliteErrorKind::Authority 469 ); 470 first.release().expect("idempotent release"); 471 let next = WriterAuthority::acquire(&paths, OpenMode::ReadWriteExisting) 472 .expect("reacquire") 473 .expect("writer capability"); 474 drop(next); 475 assert!( 476 WriterAuthority::acquire(&paths, OpenMode::ReadWriteExisting) 477 .expect("reacquire after drop") 478 .is_some() 479 ); 480 } 481 482 #[test] 483 fn retained_stale_lock_inode_is_reused_without_content_mutation() { 484 let root = tempfile::tempdir().expect("root"); 485 let paths = paths(root.path(), "stale"); 486 prepare(&paths); 487 fs::write(paths.state_lock(), b"stale-evidence").expect("stale lock"); 488 let before = fs::metadata(paths.state_lock()).expect("before metadata"); 489 490 let mut authority = WriterAuthority::acquire(&paths, OpenMode::Initialize) 491 .expect("acquire stale inode") 492 .expect("writer capability"); 493 authority.release().expect("release stale inode"); 494 495 let after = fs::metadata(paths.state_lock()).expect("after metadata"); 496 assert_eq!(before.dev(), after.dev()); 497 assert_eq!(before.ino(), after.ino()); 498 assert_eq!( 499 fs::read(paths.state_lock()).expect("stale content"), 500 b"stale-evidence" 501 ); 502 assert_eq!(after.permissions().mode() & 0o777, 0o600); 503 } 504 505 #[test] 506 fn read_only_inspection_has_no_filesystem_side_effect() { 507 let root = tempfile::tempdir().expect("root"); 508 let paths = paths(root.path(), "inspection"); 509 let state_directory = paths.state_lock().parent().expect("state directory"); 510 assert!(!state_directory.exists()); 511 assert!( 512 WriterAuthority::acquire(&paths, OpenMode::ReadOnlyInspection) 513 .expect("read-only declaration") 514 .is_none() 515 ); 516 assert!(!state_directory.exists()); 517 assert!(!paths.state_lock().exists()); 518 } 519 520 #[test] 521 fn unsafe_directory_and_lock_shapes_fail_closed() { 522 assert_eq!( 523 validate_directory(true, 10, 0o40700, 11), 524 Err(WriterAuthorityCause::StateDirectoryWrongOwner) 525 ); 526 assert_eq!( 527 validate_directory(true, 10, 0o40720, 10), 528 Err(WriterAuthorityCause::StateDirectoryInsecurePermissions) 529 ); 530 assert_eq!( 531 validate_directory(false, 10, 0o100600, 10), 532 Err(WriterAuthorityCause::StateDirectoryInvalidType) 533 ); 534 assert_eq!( 535 validate_lock(false, 1, 10, 10), 536 Err(WriterAuthorityCause::LockInvalidType) 537 ); 538 assert_eq!( 539 validate_lock(true, 2, 10, 10), 540 Err(WriterAuthorityCause::LockMultipleLinks) 541 ); 542 assert_eq!( 543 validate_lock(true, 1, 10, 11), 544 Err(WriterAuthorityCause::LockWrongOwner) 545 ); 546 547 let root = tempfile::tempdir().expect("root"); 548 let symlink_paths = paths(root.path(), "symlink"); 549 prepare(&symlink_paths); 550 let target = root.path().join("target"); 551 fs::write(&target, []).expect("target"); 552 symlink(&target, symlink_paths.state_lock()).expect("lock symlink"); 553 assert!(WriterAuthority::acquire(&symlink_paths, OpenMode::Initialize).is_err()); 554 555 let hardlink_paths = paths(root.path(), "hardlink"); 556 prepare(&hardlink_paths); 557 fs::hard_link(&target, hardlink_paths.state_lock()).expect("lock hard link"); 558 assert!(WriterAuthority::acquire(&hardlink_paths, OpenMode::Initialize).is_err()); 559 560 let directory_paths = paths(root.path(), "directory"); 561 prepare(&directory_paths); 562 fs::create_dir(directory_paths.state_lock()).expect("directory lock"); 563 assert!(WriterAuthority::acquire(&directory_paths, OpenMode::Initialize).is_err()); 564 565 let insecure_paths = paths(root.path(), "insecure"); 566 prepare(&insecure_paths); 567 fs::set_permissions( 568 insecure_paths 569 .state_lock() 570 .parent() 571 .expect("state directory"), 572 fs::Permissions::from_mode(0o722), 573 ) 574 .expect("insecure mode"); 575 assert!(WriterAuthority::acquire(&insecure_paths, OpenMode::Initialize).is_err()); 576 } 577 578 #[test] 579 fn authority_errors_and_debug_are_path_redacted() { 580 let root = tempfile::tempdir().expect("root"); 581 let sensitive_root = root.path().join("secret-state-root"); 582 fs::create_dir(&sensitive_root).expect("sensitive root"); 583 let paths = paths(&sensitive_root, "redacted"); 584 let error = WriterAuthority::acquire(&paths, OpenMode::Initialize) 585 .expect_err("missing state directory"); 586 let display = error.to_string(); 587 let debug = format!("{error:?}"); 588 let source = error.source().map(ToString::to_string).unwrap(); 589 for projection in [display, debug, source] { 590 assert!(!projection.contains("secret-state-root")); 591 assert!(!projection.contains("state.lock")); 592 assert!(!projection.contains(root.path().to_string_lossy().as_ref())); 593 } 594 595 prepare(&paths); 596 let mut authority = WriterAuthority::acquire(&paths, OpenMode::Initialize) 597 .expect("authority") 598 .expect("writer capability"); 599 assert_eq!( 600 format!("{authority:?}"), 601 "WriterAuthority { state: \"held\" }" 602 ); 603 authority.release().expect("release"); 604 assert_eq!( 605 format!("{authority:?}"), 606 "WriterAuthority { state: \"released\" }" 607 ); 608 } 609 610 #[test] 611 fn fixed_private_cause_inventory_is_path_free() { 612 let causes = [ 613 WriterAuthorityCause::StateDirectoryUnavailable, 614 WriterAuthorityCause::StateDirectoryInvalidType, 615 WriterAuthorityCause::StateDirectoryWrongOwner, 616 WriterAuthorityCause::StateDirectoryInsecurePermissions, 617 WriterAuthorityCause::LockUnavailable, 618 WriterAuthorityCause::LockInvalidType, 619 WriterAuthorityCause::LockMultipleLinks, 620 WriterAuthorityCause::LockWrongOwner, 621 WriterAuthorityCause::Contended, 622 WriterAuthorityCause::UnlockFailed, 623 ]; 624 for cause in causes { 625 let display = cause.to_string(); 626 assert!(display.is_ascii()); 627 assert!(!display.contains('/')); 628 assert!(!display.contains(".sqlite")); 629 assert!(!display.contains("state.lock")); 630 assert!(require_authority_condition(true, cause).is_ok()); 631 assert_eq!(require_authority_condition(false, cause), Err(cause)); 632 } 633 } 634 }