1use std::fmt;
7
8#[derive(Debug, thiserror::Error)]
10pub enum CoreError {
11 #[error("quota exceeded: limit={limit}, requested={requested}, resource={resource}")]
13 QuotaExceeded {
14 resource: &'static str,
16 limit: u64,
18 requested: u64,
20 },
21
22 #[error("resource not found: id={id}, type={resource_type}")]
24 ResourceNotFound {
25 id: u64,
27 resource_type: &'static str,
29 },
30
31 #[error("resource already exists: id={id}, type={resource_type}")]
33 ResourceAlreadyExists {
34 id: u64,
36 resource_type: &'static str,
38 },
39
40 #[error("resource poisoned: id={id}, reason={reason}")]
42 ResourcePoisoned {
43 id: u64,
45 reason: String,
47 },
48
49 #[error("ownership violation: expected={expected}, actual={actual}")]
51 OwnershipViolation {
52 expected: &'static str,
54 actual: &'static str,
56 },
57
58 #[error("invalid address range: addr={addr:x}, len={len}, max={max:x}")]
60 InvalidAddressRange {
61 addr: u64,
63 len: u64,
65 max: u64,
67 },
68
69 #[error("arithmetic overflow: operation={op}, a={a}, b={b}")]
71 ArithmeticOverflow {
72 op: &'static str,
74 a: u64,
76 b: u64,
78 },
79
80 #[error("state conflict: current={current}, expected={expected}")]
82 StateConflict {
83 current: String,
85 expected: String,
87 },
88
89 #[error("invalid config: {field} — {reason}")]
91 InvalidConfig {
92 field: &'static str,
94 reason: &'static str,
96 },
97
98 #[error("internal error: {0}")]
100 Internal(String),
101
102 #[error("unknown error: {0}")]
104 Unknown(String),
105}
106
107pub type CoreResult<T> = Result<T, CoreError>;
109
110impl CoreError {
111 pub fn quota_exceeded(resource: &'static str, limit: u64, requested: u64) -> Self {
113 CoreError::QuotaExceeded {
114 resource,
115 limit,
116 requested,
117 }
118 }
119
120 pub fn resource_not_found(id: u64, resource_type: &'static str) -> Self {
122 CoreError::ResourceNotFound { id, resource_type }
123 }
124
125 pub fn resource_already_exists(id: u64, resource_type: &'static str) -> Self {
127 CoreError::ResourceAlreadyExists {
128 id,
129 resource_type,
130 }
131 }
132
133 pub fn resource_poisoned(id: u64, reason: impl Into<String>) -> Self {
135 CoreError::ResourcePoisoned {
136 id,
137 reason: reason.into(),
138 }
139 }
140
141 pub fn ownership_violation(expected: &'static str, actual: &'static str) -> Self {
143 CoreError::OwnershipViolation { expected, actual }
144 }
145
146 pub fn invalid_address_range(addr: u64, len: u64, max: u64) -> Self {
148 CoreError::InvalidAddressRange { addr, len, max }
149 }
150
151 pub fn arithmetic_overflow(op: &'static str, a: u64, b: u64) -> Self {
153 CoreError::ArithmeticOverflow { op, a, b }
154 }
155
156 pub fn state_conflict(current: impl Into<String>, expected: impl Into<String>) -> Self {
158 CoreError::StateConflict {
159 current: current.into(),
160 expected: expected.into(),
161 }
162 }
163
164 pub fn invalid_config(field: &'static str, reason: &'static str) -> Self {
166 CoreError::InvalidConfig { field, reason }
167 }
168
169 pub fn internal(msg: impl Into<String>) -> Self {
171 CoreError::Internal(msg.into())
172 }
173
174 pub fn unknown(msg: impl Into<String>) -> Self {
176 CoreError::Unknown(msg.into())
177 }
178}
179
180pub trait LoggableError: fmt::Display {
182 fn is_recoverable(&self) -> bool;
184
185 fn is_security_related(&self) -> bool;
187
188 fn severity(&self) -> ErrorSeverity;
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub enum ErrorSeverity {
199 Info,
201 Warning,
203 Error,
205 Critical,
207}
208
209impl LoggableError for CoreError {
210 fn is_recoverable(&self) -> bool {
211 matches!(
212 self,
213 CoreError::QuotaExceeded { .. }
214 | CoreError::ResourceNotFound { .. }
215 | CoreError::ResourceAlreadyExists { .. }
216 )
217 }
218
219 fn is_security_related(&self) -> bool {
220 matches!(
221 self,
222 CoreError::OwnershipViolation { .. } | CoreError::InvalidAddressRange { .. }
223 )
224 }
225
226 fn severity(&self) -> ErrorSeverity {
227 match self {
228 CoreError::OwnershipViolation { .. } => ErrorSeverity::Critical,
229 CoreError::ResourcePoisoned { .. } => ErrorSeverity::Critical,
230 CoreError::InvalidAddressRange { .. } => ErrorSeverity::Error,
231 CoreError::ArithmeticOverflow { .. } => ErrorSeverity::Error,
232 CoreError::StateConflict { .. } => ErrorSeverity::Warning,
233 CoreError::InvalidConfig { .. } => ErrorSeverity::Warning,
234 CoreError::QuotaExceeded { .. } => ErrorSeverity::Warning,
235 CoreError::ResourceAlreadyExists { .. } => ErrorSeverity::Warning,
236 CoreError::ResourceNotFound { .. } => ErrorSeverity::Info,
237 CoreError::Internal(_) => ErrorSeverity::Error,
238 CoreError::Unknown(_) => ErrorSeverity::Error,
239 }
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn test_quota_exceeded_error() {
249 let err = CoreError::quota_exceeded("frame", 1024, 2048);
250 assert!(err.is_recoverable());
251 assert!(!err.is_security_related());
252 assert_eq!(err.severity(), ErrorSeverity::Warning);
253 assert!(err.to_string().contains("frame"));
254 }
255
256 #[test]
257 fn test_ownership_violation_error() {
258 let err = CoreError::ownership_violation("pool", "other");
259 assert!(!err.is_recoverable());
260 assert!(err.is_security_related());
261 assert_eq!(err.severity(), ErrorSeverity::Critical);
262 }
263
264 #[test]
265 fn test_invalid_address_range_error() {
266 let err = CoreError::invalid_address_range(0x1000, 256, 0x2000);
267 assert!(!err.is_recoverable());
268 assert!(err.is_security_related());
269 assert_eq!(err.severity(), ErrorSeverity::Error);
270 }
271
272 #[test]
273 fn test_arithmetic_overflow_error() {
274 let err = CoreError::arithmetic_overflow("add", u64::MAX, 1);
275 assert_eq!(err.severity(), ErrorSeverity::Error);
276 }
277
278 #[test]
279 fn test_state_conflict_error() {
280 let err = CoreError::state_conflict("active", "idle");
281 assert_eq!(err.severity(), ErrorSeverity::Warning);
282 }
283
284 #[test]
285 fn test_internal_error() {
286 let err = CoreError::internal("something went wrong");
287 assert_eq!(err.severity(), ErrorSeverity::Error);
288 }
289
290 #[test]
293 fn test_quota_exceeded_display() {
294 let err = CoreError::quota_exceeded("memory", 1024, 2048);
295 let msg = err.to_string();
296 assert!(msg.contains("quota exceeded"));
297 assert!(msg.contains("memory"));
298 assert!(msg.contains("1024"));
299 assert!(msg.contains("2048"));
300 }
301
302 #[test]
303 fn test_resource_not_found_constructor_and_display() {
304 let err = CoreError::resource_not_found(42, "frame");
305 assert!(err.is_recoverable());
306 assert!(!err.is_security_related());
307 assert_eq!(err.severity(), ErrorSeverity::Info);
308
309 let msg = err.to_string();
310 assert!(msg.contains("resource not found"));
311 assert!(msg.contains("42"));
312 assert!(msg.contains("frame"));
313 }
314
315 #[test]
316 fn test_resource_already_exists_constructor_and_display() {
317 let err = CoreError::resource_already_exists(7, "connection");
318 assert!(err.is_recoverable());
319 assert!(!err.is_security_related());
320 assert_eq!(err.severity(), ErrorSeverity::Warning);
321
322 let msg = err.to_string();
323 assert!(msg.contains("resource already exists"));
324 assert!(msg.contains("7"));
325 assert!(msg.contains("connection"));
326 }
327
328 #[test]
329 fn test_resource_poisoned_constructor_and_display() {
330 let err = CoreError::resource_poisoned(100, "corrupted data");
331 assert!(!err.is_recoverable());
332 assert!(!err.is_security_related());
333 assert_eq!(err.severity(), ErrorSeverity::Critical);
334
335 let msg = err.to_string();
336 assert!(msg.contains("resource poisoned"));
337 assert!(msg.contains("100"));
338 assert!(msg.contains("corrupted data"));
339 }
340
341 #[test]
342 fn test_ownership_violation_display() {
343 let err = CoreError::ownership_violation("pool_a", "pool_b");
344 let msg = err.to_string();
345 assert!(msg.contains("ownership violation"));
346 assert!(msg.contains("pool_a"));
347 assert!(msg.contains("pool_b"));
348 }
349
350 #[test]
351 fn test_invalid_address_range_display() {
352 let err = CoreError::invalid_address_range(0x1000, 256, 0x2000);
353 let msg = err.to_string();
354 assert!(msg.contains("invalid address range"));
355 assert!(msg.contains(&format!("{:x}", 0x1000)));
356 assert!(msg.contains("256"));
357 }
358
359 #[test]
360 fn test_arithmetic_overflow_display() {
361 let err = CoreError::arithmetic_overflow("mul", 100, 200);
362 let msg = err.to_string();
363 assert!(msg.contains("arithmetic overflow"));
364 assert!(msg.contains("mul"));
365 assert!(msg.contains("100"));
366 assert!(msg.contains("200"));
367 }
368
369 #[test]
370 fn test_state_conflict_constructor_and_display() {
371 let err = CoreError::state_conflict("running", "stopped");
372 assert!(!err.is_recoverable());
373 assert!(!err.is_security_related());
374 assert_eq!(err.severity(), ErrorSeverity::Warning);
375
376 let msg = err.to_string();
377 assert!(msg.contains("state conflict"));
378 assert!(msg.contains("running"));
379 assert!(msg.contains("stopped"));
380 }
381
382 #[test]
383 fn test_internal_error_constructor_and_display() {
384 let err = CoreError::internal("fatal crash");
385 assert!(!err.is_recoverable());
386 assert!(!err.is_security_related());
387 assert_eq!(err.severity(), ErrorSeverity::Error);
388
389 let msg = err.to_string();
390 assert!(msg.contains("internal error"));
391 assert!(msg.contains("fatal crash"));
392 }
393
394 #[test]
395 fn test_unknown_error_constructor_and_display() {
396 let err = CoreError::unknown("mystery error");
397 assert!(!err.is_recoverable());
398 assert!(!err.is_security_related());
399 assert_eq!(err.severity(), ErrorSeverity::Error);
400
401 let msg = err.to_string();
402 assert!(msg.contains("unknown error"));
403 assert!(msg.contains("mystery error"));
404 }
405
406 #[test]
409 fn test_error_severity_equality() {
410 assert_eq!(ErrorSeverity::Info, ErrorSeverity::Info);
411 assert_eq!(ErrorSeverity::Warning, ErrorSeverity::Warning);
412 assert_eq!(ErrorSeverity::Error, ErrorSeverity::Error);
413 assert_eq!(ErrorSeverity::Critical, ErrorSeverity::Critical);
414 }
415
416 #[test]
417 fn test_error_severity_clone_copy() {
418 let s = ErrorSeverity::Warning;
419 let s2 = s;
420 assert_eq!(s, s2);
421 let s3 = s;
422 assert_eq!(s, s3);
423 }
424
425 #[test]
426 fn test_error_severity_debug() {
427 let s = format!("{:?}", ErrorSeverity::Critical);
428 assert_eq!(s, "Critical");
429 }
430
431 #[test]
434 fn test_all_recoverable_errors() {
435 assert!(CoreError::quota_exceeded("mem", 0, 0).is_recoverable());
436 assert!(CoreError::resource_not_found(0, "x").is_recoverable());
437 assert!(CoreError::resource_already_exists(0, "x").is_recoverable());
438
439 assert!(!CoreError::resource_poisoned(0, "x").is_recoverable());
440 assert!(!CoreError::ownership_violation("a", "b").is_recoverable());
441 assert!(!CoreError::invalid_address_range(0, 0, 0).is_recoverable());
442 assert!(!CoreError::arithmetic_overflow("add", 0, 0).is_recoverable());
443 assert!(!CoreError::state_conflict("a", "b").is_recoverable());
444 assert!(!CoreError::internal("x").is_recoverable());
445 assert!(!CoreError::unknown("x").is_recoverable());
446 }
447
448 #[test]
449 fn test_all_security_related_errors() {
450 assert!(CoreError::ownership_violation("a", "b").is_security_related());
451 assert!(CoreError::invalid_address_range(0, 0, 0).is_security_related());
452
453 assert!(!CoreError::quota_exceeded("mem", 0, 0).is_security_related());
454 assert!(!CoreError::resource_not_found(0, "x").is_security_related());
455 assert!(!CoreError::resource_already_exists(0, "x").is_security_related());
456 assert!(!CoreError::resource_poisoned(0, "x").is_security_related());
457 assert!(!CoreError::arithmetic_overflow("add", 0, 0).is_security_related());
458 assert!(!CoreError::state_conflict("a", "b").is_security_related());
459 assert!(!CoreError::internal("x").is_security_related());
460 assert!(!CoreError::unknown("x").is_security_related());
461 }
462
463 #[test]
466 fn test_loggable_error_trait_object() {
467 let err: Box<dyn LoggableError> = Box::new(CoreError::internal("test"));
468 assert!(!err.is_recoverable());
469 assert!(!err.is_security_related());
470 assert_eq!(err.severity(), ErrorSeverity::Error);
471 assert!(err.to_string().contains("internal error"));
472 }
473}