lib

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

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(&current_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         &current_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(&current_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 }