lib

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

error.rs (10056B)


      1 //! Stable safe errors for shared SQLite mechanics.
      2 
      3 use core::fmt;
      4 use std::error::Error;
      5 
      6 use serde::{Serialize, Serializer};
      7 
      8 const MAX_SAFE_ERROR_MESSAGE_BYTES: usize = 96;
      9 
     10 /// Stable public codes for service-neutral SQLite failures.
     11 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     12 pub enum ServiceSqliteErrorCode {
     13     Authority,
     14     Open,
     15     Create,
     16     Pragma,
     17     Metadata,
     18     Migration,
     19     Backup,
     20     Restore,
     21     Integrity,
     22     Recovery,
     23 }
     24 
     25 impl ServiceSqliteErrorCode {
     26     /// Returns the stable lowercase wire representation.
     27     #[must_use]
     28     pub const fn as_str(self) -> &'static str {
     29         match self {
     30             Self::Authority => "sqlite_authority",
     31             Self::Open => "sqlite_open",
     32             Self::Create => "sqlite_create",
     33             Self::Pragma => "sqlite_pragma",
     34             Self::Metadata => "sqlite_metadata",
     35             Self::Migration => "sqlite_migration",
     36             Self::Backup => "sqlite_backup",
     37             Self::Restore => "sqlite_restore",
     38             Self::Integrity => "sqlite_integrity",
     39             Self::Recovery => "sqlite_recovery",
     40         }
     41     }
     42 }
     43 
     44 impl fmt::Display for ServiceSqliteErrorCode {
     45     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     46         formatter.write_str(self.as_str())
     47     }
     48 }
     49 
     50 impl Serialize for ServiceSqliteErrorCode {
     51     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     52     where
     53         S: Serializer,
     54     {
     55         serializer.serialize_str(self.as_str())
     56     }
     57 }
     58 
     59 /// Service-neutral failure classes used by the SQLite mechanism boundary.
     60 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     61 pub enum ServiceSqliteErrorKind {
     62     Authority,
     63     Open,
     64     Create,
     65     Pragma,
     66     Metadata,
     67     Migration,
     68     Backup,
     69     Restore,
     70     Integrity,
     71     Recovery,
     72 }
     73 
     74 impl ServiceSqliteErrorKind {
     75     /// Returns the stable public code for this failure class.
     76     #[must_use]
     77     pub const fn code(self) -> ServiceSqliteErrorCode {
     78         match self {
     79             Self::Authority => ServiceSqliteErrorCode::Authority,
     80             Self::Open => ServiceSqliteErrorCode::Open,
     81             Self::Create => ServiceSqliteErrorCode::Create,
     82             Self::Pragma => ServiceSqliteErrorCode::Pragma,
     83             Self::Metadata => ServiceSqliteErrorCode::Metadata,
     84             Self::Migration => ServiceSqliteErrorCode::Migration,
     85             Self::Backup => ServiceSqliteErrorCode::Backup,
     86             Self::Restore => ServiceSqliteErrorCode::Restore,
     87             Self::Integrity => ServiceSqliteErrorCode::Integrity,
     88             Self::Recovery => ServiceSqliteErrorCode::Recovery,
     89         }
     90     }
     91 
     92     /// Returns the bounded projection safe for logs, status, and wire envelopes.
     93     #[must_use]
     94     pub const fn safe_error(self) -> SafeServiceSqliteError {
     95         let message = match self {
     96             Self::Authority => "SQLite writer authority could not be established",
     97             Self::Open => "SQLite state could not be opened",
     98             Self::Create => "SQLite state could not be created",
     99             Self::Pragma => "SQLite pragma policy could not be applied",
    100             Self::Metadata => "SQLite metadata is invalid",
    101             Self::Migration => "SQLite migration history is invalid",
    102             Self::Backup => "SQLite backup failed",
    103             Self::Restore => "SQLite restore failed",
    104             Self::Integrity => "SQLite integrity verification failed",
    105             Self::Recovery => "SQLite recovery failed",
    106         };
    107         SafeServiceSqliteError::new(self.code(), message)
    108     }
    109 }
    110 
    111 /// Bounded service-neutral SQLite error safe for public observation.
    112 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
    113 pub struct SafeServiceSqliteError {
    114     code: ServiceSqliteErrorCode,
    115     message: &'static str,
    116 }
    117 
    118 impl SafeServiceSqliteError {
    119     const fn new(code: ServiceSqliteErrorCode, message: &'static str) -> Self {
    120         assert!(message.is_ascii());
    121         assert!(message.len() <= MAX_SAFE_ERROR_MESSAGE_BYTES);
    122         Self { code, message }
    123     }
    124 
    125     /// Returns the typed stable error code.
    126     #[must_use]
    127     pub const fn code(self) -> ServiceSqliteErrorCode {
    128         self.code
    129     }
    130 
    131     /// Returns the stable serialized code.
    132     #[must_use]
    133     pub const fn code_str(self) -> &'static str {
    134         self.code.as_str()
    135     }
    136 
    137     /// Returns the bounded safe message.
    138     #[must_use]
    139     pub const fn message(self) -> &'static str {
    140         self.message
    141     }
    142 }
    143 
    144 impl fmt::Display for SafeServiceSqliteError {
    145     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    146         write!(formatter, "{}: {}", self.code, self.message)
    147     }
    148 }
    149 
    150 /// SQLite mechanism failure with an optional cause for trusted inspection.
    151 pub struct ServiceSqliteError {
    152     kind: ServiceSqliteErrorKind,
    153     source: Option<Box<dyn Error + Send + Sync + 'static>>,
    154 }
    155 
    156 impl ServiceSqliteError {
    157     /// Creates a failure without an upstream cause.
    158     #[must_use]
    159     pub const fn new(kind: ServiceSqliteErrorKind) -> Self {
    160         Self { kind, source: None }
    161     }
    162 
    163     /// Creates a failure while retaining its cause for trusted inspection.
    164     pub fn with_source(
    165         kind: ServiceSqliteErrorKind,
    166         source: impl Error + Send + Sync + 'static,
    167     ) -> Self {
    168         Self {
    169             kind,
    170             source: Some(Box::new(source)),
    171         }
    172     }
    173 
    174     /// Returns the stable service-neutral failure class.
    175     #[must_use]
    176     pub const fn kind(&self) -> ServiceSqliteErrorKind {
    177         self.kind
    178     }
    179 
    180     /// Returns the bounded projection safe for public observation.
    181     #[must_use]
    182     pub const fn safe_error(&self) -> SafeServiceSqliteError {
    183         self.kind.safe_error()
    184     }
    185 }
    186 
    187 impl fmt::Debug for ServiceSqliteError {
    188     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    189         formatter
    190             .debug_struct("ServiceSqliteError")
    191             .field("kind", &self.kind)
    192             .field("safe_error", &self.safe_error())
    193             .field("source", &self.source.as_ref().map(|_| "[redacted]"))
    194             .finish()
    195     }
    196 }
    197 
    198 impl fmt::Display for ServiceSqliteError {
    199     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    200         self.safe_error().fmt(formatter)
    201     }
    202 }
    203 
    204 impl Error for ServiceSqliteError {
    205     fn source(&self) -> Option<&(dyn Error + 'static)> {
    206         self.source
    207             .as_deref()
    208             .map(|source| source as &(dyn Error + 'static))
    209     }
    210 }
    211 
    212 #[cfg(test)]
    213 mod tests {
    214     use super::*;
    215 
    216     #[derive(Debug)]
    217     struct SensitiveCause;
    218 
    219     impl fmt::Display for SensitiveCause {
    220         fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    221             formatter.write_str("secret path=/private/state.sqlite")
    222         }
    223     }
    224 
    225     impl Error for SensitiveCause {}
    226 
    227     #[test]
    228     fn error_inventory_codes_messages_and_serialization_are_exact() {
    229         let inventory = [
    230             (
    231                 ServiceSqliteErrorKind::Authority,
    232                 "sqlite_authority",
    233                 "SQLite writer authority could not be established",
    234             ),
    235             (
    236                 ServiceSqliteErrorKind::Open,
    237                 "sqlite_open",
    238                 "SQLite state could not be opened",
    239             ),
    240             (
    241                 ServiceSqliteErrorKind::Create,
    242                 "sqlite_create",
    243                 "SQLite state could not be created",
    244             ),
    245             (
    246                 ServiceSqliteErrorKind::Pragma,
    247                 "sqlite_pragma",
    248                 "SQLite pragma policy could not be applied",
    249             ),
    250             (
    251                 ServiceSqliteErrorKind::Metadata,
    252                 "sqlite_metadata",
    253                 "SQLite metadata is invalid",
    254             ),
    255             (
    256                 ServiceSqliteErrorKind::Migration,
    257                 "sqlite_migration",
    258                 "SQLite migration history is invalid",
    259             ),
    260             (
    261                 ServiceSqliteErrorKind::Backup,
    262                 "sqlite_backup",
    263                 "SQLite backup failed",
    264             ),
    265             (
    266                 ServiceSqliteErrorKind::Restore,
    267                 "sqlite_restore",
    268                 "SQLite restore failed",
    269             ),
    270             (
    271                 ServiceSqliteErrorKind::Integrity,
    272                 "sqlite_integrity",
    273                 "SQLite integrity verification failed",
    274             ),
    275             (
    276                 ServiceSqliteErrorKind::Recovery,
    277                 "sqlite_recovery",
    278                 "SQLite recovery failed",
    279             ),
    280         ];
    281 
    282         for (kind, code, message) in inventory {
    283             let safe = kind.safe_error();
    284             assert_eq!(kind.code().as_str(), code);
    285             assert_eq!(kind.code().to_string(), code);
    286             assert_eq!(safe.code(), kind.code());
    287             assert_eq!(safe.code_str(), code);
    288             assert_eq!(safe.message(), message);
    289             assert!(message.is_ascii());
    290             assert!(message.len() <= MAX_SAFE_ERROR_MESSAGE_BYTES);
    291             assert_eq!(
    292                 serde_json::to_string(&kind.code()).unwrap(),
    293                 format!(r#""{code}""#)
    294             );
    295             assert_eq!(
    296                 serde_json::to_string(&safe).unwrap(),
    297                 format!(r#"{{"code":"{code}","message":"{message}"}}"#)
    298             );
    299         }
    300     }
    301 
    302     #[test]
    303     fn raw_error_redacts_but_preserves_its_trusted_source() {
    304         let error = ServiceSqliteError::with_source(ServiceSqliteErrorKind::Open, SensitiveCause);
    305         let display = error.to_string();
    306         let debug = format!("{error:?}");
    307         let serialized = serde_json::to_string(&error.safe_error()).unwrap();
    308 
    309         for public in [&display, &debug, &serialized] {
    310             assert!(!public.contains("secret"));
    311             assert!(!public.contains("private"));
    312             assert!(!public.contains("state.sqlite"));
    313         }
    314         assert_eq!(
    315             error.source().map(ToString::to_string).as_deref(),
    316             Some("secret path=/private/state.sqlite")
    317         );
    318         assert_eq!(error.kind(), ServiceSqliteErrorKind::Open);
    319     }
    320 }