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 }