shadowforge_lib/domain/errors.rs
1//! Domain error types for all bounded contexts.
2//!
3//! Every error variant uses [`thiserror`] and serialises cleanly to JSON.
4//! No I/O errors live here — those are adapter concerns.
5
6use thiserror::Error;
7
8// ─── CryptoError ──────────────────────────────────────────────────────────────
9
10/// Errors produced by the crypto bounded context.
11#[derive(Debug, Error)]
12pub enum CryptoError {
13 /// Key generation failed.
14 #[error("key generation failed: {reason}")]
15 KeyGenFailed {
16 /// Human-readable reason for the failure.
17 reason: String,
18 },
19 /// Key encapsulation failed.
20 #[error("encapsulation failed: {reason}")]
21 EncapsulationFailed {
22 /// Human-readable reason for the failure.
23 reason: String,
24 },
25 /// Key decapsulation failed.
26 #[error("decapsulation failed: {reason}")]
27 DecapsulationFailed {
28 /// Human-readable reason for the failure.
29 reason: String,
30 },
31 /// Signature creation failed.
32 #[error("signing failed: {reason}")]
33 SigningFailed {
34 /// Human-readable reason for the failure.
35 reason: String,
36 },
37 /// Signature verification failed (bad key, corrupted data, or forgery).
38 #[error("signature verification failed: {reason}")]
39 VerificationFailed {
40 /// Human-readable reason for the failure.
41 reason: String,
42 },
43 /// AES-GCM encryption failed.
44 #[error("encryption failed: {reason}")]
45 EncryptionFailed {
46 /// Human-readable reason for the failure.
47 reason: String,
48 },
49 /// AES-GCM decryption or authentication-tag verification failed.
50 #[error("decryption failed: {reason}")]
51 DecryptionFailed {
52 /// Human-readable reason for the failure.
53 reason: String,
54 },
55 /// KDF derivation failed (e.g. invalid Argon2 parameters).
56 #[error("KDF failed: {reason}")]
57 KdfFailed {
58 /// Human-readable reason for the failure.
59 reason: String,
60 },
61 /// Input key material was the wrong length.
62 #[error("invalid key length: expected {expected} bytes, got {got}")]
63 InvalidKeyLength {
64 /// Expected key length in bytes.
65 expected: usize,
66 /// Actual key length provided.
67 got: usize,
68 },
69 /// Nonce was the wrong length.
70 #[error("invalid nonce length: expected {expected} bytes, got {got}")]
71 InvalidNonceLength {
72 /// Expected nonce length in bytes.
73 expected: usize,
74 /// Actual nonce length provided.
75 got: usize,
76 },
77}
78
79// ─── CorrectionError ──────────────────────────────────────────────────────────
80
81/// Errors produced by the error-correction bounded context.
82#[derive(Debug, Error)]
83pub enum CorrectionError {
84 /// Too few shards survive to reconstruct the original data.
85 #[error("insufficient shards: need {needed}, have {available}")]
86 InsufficientShards {
87 /// Minimum number of data shards required.
88 needed: usize,
89 /// Number of valid shards available.
90 available: usize,
91 },
92 /// An HMAC tag did not match the shard contents.
93 #[error("HMAC mismatch on shard {index}")]
94 HmacMismatch {
95 /// Zero-based shard index whose tag failed validation.
96 index: u8,
97 },
98 /// The Reed-Solomon library reported an unrecoverable error.
99 #[error("reed-solomon error: {reason}")]
100 ReedSolomonError {
101 /// Human-readable reason for the failure.
102 reason: String,
103 },
104 /// Shard set parameters are invalid (e.g. zero data shards).
105 #[error("invalid shard parameters: {reason}")]
106 InvalidParameters {
107 /// Human-readable reason the parameters are invalid.
108 reason: String,
109 },
110}
111
112// ─── StegoError ───────────────────────────────────────────────────────────────
113
114/// Errors produced by the steganography bounded context.
115#[derive(Debug, Error)]
116pub enum StegoError {
117 /// The payload is too large for the selected cover and technique.
118 #[error("payload too large: need {needed} bytes, cover holds {available}")]
119 PayloadTooLarge {
120 /// Number of bytes the payload requires.
121 needed: u64,
122 /// Number of bytes the cover can hold with this technique.
123 available: u64,
124 },
125 /// The cover medium type is incompatible with the selected technique.
126 #[error("unsupported cover type for this technique: {reason}")]
127 UnsupportedCoverType {
128 /// Human-readable description of the incompatibility.
129 reason: String,
130 },
131 /// Raw pixel or sample data is malformed or truncated.
132 #[error("malformed cover data: {reason}")]
133 MalformedCoverData {
134 /// Human-readable reason the data is malformed.
135 reason: String,
136 },
137 /// No hidden payload was found during extraction.
138 #[error("no payload found in stego cover")]
139 NoPayloadFound,
140 /// Extraction produced data that failed integrity checks.
141 #[error("extracted data failed integrity check: {reason}")]
142 IntegrityCheckFailed {
143 /// Human-readable description of what failed.
144 reason: String,
145 },
146}
147
148// ─── MediaError ───────────────────────────────────────────────────────────────
149
150/// Errors produced by the media bounded context.
151#[derive(Debug, Error)]
152pub enum MediaError {
153 /// The file format is not supported by any registered codec.
154 #[error("unsupported media format: {extension}")]
155 UnsupportedFormat {
156 /// File extension or MIME type that was rejected.
157 extension: String,
158 },
159 /// Decoding the raw file bytes failed.
160 #[error("decode error: {reason}")]
161 DecodeFailed {
162 /// Human-readable reason for the decode failure.
163 reason: String,
164 },
165 /// Encoding the decoded data back to bytes failed.
166 #[error("encode error: {reason}")]
167 EncodeFailed {
168 /// Human-readable reason for the encode failure.
169 reason: String,
170 },
171 /// A filesystem path was invalid or unreadable (adapter-level only).
172 #[error("IO error: {reason}")]
173 IoError {
174 /// Human-readable description of the IO problem.
175 reason: String,
176 },
177}
178
179// ─── PdfError ─────────────────────────────────────────────────────────────────
180
181/// Errors produced by the PDF bounded context.
182#[derive(Debug, Error)]
183pub enum PdfError {
184 /// The PDF document could not be parsed.
185 #[error("PDF parse error: {reason}")]
186 ParseFailed {
187 /// Human-readable reason for the parse failure.
188 reason: String,
189 },
190 /// Page rasterisation via pdfium failed.
191 #[error("page render error on page {page}: {reason}")]
192 RenderFailed {
193 /// Zero-based page index that failed to render.
194 page: usize,
195 /// Human-readable reason for the render failure.
196 reason: String,
197 },
198 /// Rebuilding a PDF from rasterised pages failed.
199 #[error("PDF rebuild error: {reason}")]
200 RebuildFailed {
201 /// Human-readable reason for the rebuild failure.
202 reason: String,
203 },
204 /// Content-stream or metadata embedding failed.
205 #[error("PDF embed error: {reason}")]
206 EmbedFailed {
207 /// Human-readable reason for the embed failure.
208 reason: String,
209 },
210 /// Content-stream or metadata extraction failed.
211 #[error("PDF extract error: {reason}")]
212 ExtractFailed {
213 /// Human-readable reason for the extract failure.
214 reason: String,
215 },
216 /// A filesystem path was invalid or unreadable (adapter-level only).
217 #[error("IO error: {reason}")]
218 IoError {
219 /// Human-readable description of the IO problem.
220 reason: String,
221 },
222 /// The PDF document is encrypted and cannot be processed.
223 #[error("PDF is encrypted and cannot be processed")]
224 Encrypted,
225}
226
227// ─── DistributionError ────────────────────────────────────────────────────────
228
229/// Errors produced by the distribution bounded context.
230#[derive(Debug, Error)]
231pub enum DistributionError {
232 /// Fewer covers were provided than the distribution pattern requires.
233 #[error("insufficient covers: need {needed}, got {got}")]
234 InsufficientCovers {
235 /// Minimum number of covers the pattern needs.
236 needed: usize,
237 /// Number of covers actually provided.
238 got: usize,
239 },
240 /// An embedding step failed during distribution.
241 #[error("embed failed on cover {index}: {source}")]
242 EmbedFailed {
243 /// Zero-based cover index at which embedding failed.
244 index: usize,
245 /// The underlying stego error.
246 #[source]
247 source: StegoError,
248 },
249 /// Error-correction encoding failed during shard production.
250 #[error("error correction failed: {source}")]
251 CorrectionFailed {
252 /// The underlying correction error.
253 #[source]
254 source: CorrectionError,
255 },
256}
257
258// ─── ReconstructionError ──────────────────────────────────────────────────────
259
260/// Errors produced by the reconstruction bounded context.
261#[derive(Debug, Error)]
262pub enum ReconstructionError {
263 /// Not enough valid stego covers were provided.
264 #[error("insufficient covers for reconstruction: need {needed}, got {got}")]
265 InsufficientCovers {
266 /// Minimum required covers.
267 needed: usize,
268 /// Covers actually provided.
269 got: usize,
270 },
271 /// Payload extraction from a stego cover failed.
272 #[error("extraction failed on cover {index}: {source}")]
273 ExtractionFailed {
274 /// Zero-based cover index at which extraction failed.
275 index: usize,
276 /// The underlying stego error.
277 #[source]
278 source: StegoError,
279 },
280 /// Error-correction decoding failed.
281 #[error("error correction failed: {source}")]
282 CorrectionFailed {
283 /// The underlying correction error.
284 #[source]
285 source: CorrectionError,
286 },
287 /// Manifest signature verification failed.
288 #[error("manifest verification failed: {reason}")]
289 ManifestVerificationFailed {
290 /// Human-readable reason the manifest failed.
291 reason: String,
292 },
293}
294
295// ─── AnalysisError ────────────────────────────────────────────────────────────
296
297/// Errors produced by the analysis bounded context.
298#[derive(Debug, Error)]
299pub enum AnalysisError {
300 /// The cover medium type is incompatible with the requested technique.
301 #[error("unsupported cover type for analysis: {reason}")]
302 UnsupportedCoverType {
303 /// Human-readable description of the incompatibility.
304 reason: String,
305 },
306 /// Statistical computation failed (e.g. divide-by-zero on empty cover).
307 #[error("statistical computation failed: {reason}")]
308 ComputationFailed {
309 /// Human-readable reason for the failure.
310 reason: String,
311 },
312}
313
314// ─── ArchiveError ─────────────────────────────────────────────────────────────
315
316/// Errors produced by the archive bounded context.
317#[derive(Debug, Error)]
318pub enum ArchiveError {
319 /// Packing files into the archive failed.
320 #[error("archive pack error: {reason}")]
321 PackFailed {
322 /// Human-readable reason for the pack failure.
323 reason: String,
324 },
325 /// Unpacking the archive failed.
326 #[error("archive unpack error: {reason}")]
327 UnpackFailed {
328 /// Human-readable reason for the unpack failure.
329 reason: String,
330 },
331 /// The archive format is not supported.
332 #[error("unsupported archive format: {reason}")]
333 UnsupportedFormat {
334 /// Human-readable description of the unsupported format.
335 reason: String,
336 },
337}
338
339// ─── AdaptiveError ────────────────────────────────────────────────────────────
340
341/// Errors produced by the adaptive embedding bounded context.
342#[derive(Debug, Error)]
343pub enum AdaptiveError {
344 /// The optimiser failed to find an embedding that meets the detectability budget.
345 #[error(
346 "could not meet detectability budget of {target_db:.2} dB: best was {achieved_db:.2} dB"
347 )]
348 BudgetNotMet {
349 /// Target detectability ceiling in decibels.
350 target_db: f64,
351 /// Best detectability achieved in decibels.
352 achieved_db: f64,
353 },
354 /// Camera model fingerprint matching failed.
355 #[error("profile matching failed: {reason}")]
356 ProfileMatchFailed {
357 /// Human-readable reason for the failure.
358 reason: String,
359 },
360 /// Compression simulation failed.
361 #[error("compression simulation failed: {reason}")]
362 CompressionSimFailed {
363 /// Human-readable reason for the failure.
364 reason: String,
365 },
366 /// The underlying stego operation failed.
367 #[error("stego error during adaptive optimisation: {source}")]
368 StegoFailed {
369 /// The underlying stego error.
370 #[source]
371 source: StegoError,
372 },
373}
374
375// ─── DeniableError ────────────────────────────────────────────────────────────
376
377/// Errors produced by the deniable steganography bounded context.
378#[derive(Debug, Error)]
379pub enum DeniableError {
380 /// The cover cannot hold both the real and decoy payload simultaneously.
381 #[error("cover capacity too small for dual-payload embedding")]
382 InsufficientCapacity,
383 /// Embedding one of the payloads failed.
384 #[error("dual embed failed: {reason}")]
385 EmbedFailed {
386 /// Human-readable reason for the failure.
387 reason: String,
388 },
389 /// Extraction with the provided key failed.
390 #[error("extraction failed for provided key: {reason}")]
391 ExtractionFailed {
392 /// Human-readable reason for the failure.
393 reason: String,
394 },
395}
396
397// ─── OpsecError ───────────────────────────────────────────────────────────────
398
399/// Errors produced by the operational security bounded context.
400#[derive(Debug, Error)]
401pub enum OpsecError {
402 /// A wipe step failed but execution continued.
403 #[error("wipe step failed for path {path}: {reason}")]
404 WipeStepFailed {
405 /// Path that could not be wiped.
406 path: String,
407 /// Human-readable reason for the failure.
408 reason: String,
409 },
410 /// The in-memory pipeline produced corrupt output.
411 #[error("amnesiac pipeline error: {reason}")]
412 PipelineError {
413 /// Human-readable reason for the failure.
414 reason: String,
415 },
416 /// Forensic watermark embedding or identification failed.
417 #[error("watermark error: {reason}")]
418 WatermarkError {
419 /// Human-readable reason for the failure.
420 reason: String,
421 },
422 /// Geographic manifest validation failed.
423 #[error("geographic manifest error: {reason}")]
424 ManifestError {
425 /// Human-readable reason for the failure.
426 reason: String,
427 },
428}
429
430// ─── CanaryError ──────────────────────────────────────────────────────────────
431
432/// Errors produced by the canary bounded context.
433#[derive(Debug, Error)]
434pub enum CanaryError {
435 /// No covers were provided to embed the canary shard into.
436 #[error("no covers provided for canary embedding")]
437 NoCovers,
438 /// Embedding the canary shard failed.
439 #[error("canary embed failed: {source}")]
440 EmbedFailed {
441 /// The underlying stego error.
442 #[source]
443 source: StegoError,
444 },
445}
446
447// ─── DeadDropError ────────────────────────────────────────────────────────────
448
449/// Errors produced by the dead drop bounded context.
450#[derive(Debug, Error)]
451pub enum DeadDropError {
452 /// The platform profile is unknown or unsupported.
453 #[error("unsupported platform for dead drop: {reason}")]
454 UnsupportedPlatform {
455 /// Human-readable description of the issue.
456 reason: String,
457 },
458 /// Encoding for the platform failed.
459 #[error("dead drop encode failed: {reason}")]
460 EncodeFailed {
461 /// Human-readable reason for the failure.
462 reason: String,
463 },
464}
465
466// ─── TimeLockError ────────────────────────────────────────────────────────────
467
468/// Errors produced by the time-lock bounded context.
469#[derive(Debug, Error)]
470pub enum TimeLockError {
471 /// The puzzle is not yet solvable (unlock time has not been reached).
472 #[error("time-lock puzzle not yet solvable; unlock at {unlock_at}")]
473 NotYetSolvable {
474 /// ISO 8601 string of the earliest unlock time.
475 unlock_at: String,
476 },
477 /// The sequential squaring computation overflowed or failed.
478 #[error("puzzle computation failed: {reason}")]
479 ComputationFailed {
480 /// Human-readable reason for the failure.
481 reason: String,
482 },
483 /// Decryption of the time-locked ciphertext failed.
484 #[error("time-lock decrypt failed: {source}")]
485 DecryptFailed {
486 /// The underlying crypto error.
487 #[source]
488 source: CryptoError,
489 },
490}
491
492// ─── ScrubberError ────────────────────────────────────────────────────────────
493
494/// Errors produced by the stylometric scrubber bounded context.
495#[derive(Debug, Error)]
496pub enum ScrubberError {
497 /// The input text is not valid UTF-8.
498 #[error("input is not valid UTF-8: {reason}")]
499 InvalidUtf8 {
500 /// Human-readable reason for the failure.
501 reason: String,
502 },
503 /// Scrubbing failed to satisfy the target stylometric profile.
504 #[error("could not satisfy stylo profile: {reason}")]
505 ProfileNotSatisfied {
506 /// Human-readable reason the profile could not be satisfied.
507 reason: String,
508 },
509}
510
511// ─── CorpusError ──────────────────────────────────────────────────────────────
512
513/// Errors produced by the corpus steganography bounded context.
514#[derive(Debug, Error)]
515pub enum CorpusError {
516 /// No suitable corpus entry was found for the given payload.
517 #[error("no suitable corpus cover found for payload of {payload_bytes} bytes")]
518 NoSuitableCover {
519 /// Size of the payload in bytes.
520 payload_bytes: u64,
521 },
522 /// The corpus index file is missing or corrupt.
523 #[error("corpus index error: {reason}")]
524 IndexError {
525 /// Human-readable description of the index problem.
526 reason: String,
527 },
528 /// A file could not be added to the corpus index.
529 #[error("corpus add failed for path {path}: {reason}")]
530 AddFailed {
531 /// The path that could not be indexed.
532 path: String,
533 /// Human-readable reason for the failure.
534 reason: String,
535 },
536}
537
538// ─── Tests ────────────────────────────────────────────────────────────────────
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543
544 #[test]
545 fn crypto_error_display_does_not_panic() {
546 let e = CryptoError::InvalidKeyLength {
547 expected: 32,
548 got: 16,
549 };
550 assert!(e.to_string().contains("32"));
551 }
552
553 #[test]
554 fn correction_error_display_does_not_panic() {
555 let e = CorrectionError::InsufficientShards {
556 needed: 5,
557 available: 2,
558 };
559 assert!(e.to_string().contains('5'));
560 }
561
562 #[test]
563 fn stego_error_display_does_not_panic() {
564 let e = StegoError::PayloadTooLarge {
565 needed: 1024,
566 available: 512,
567 };
568 assert!(e.to_string().contains("1024"));
569 }
570
571 #[test]
572 fn all_error_variants_display_without_panic() {
573 let errors: Vec<Box<dyn std::error::Error>> = vec![
574 Box::new(CryptoError::KeyGenFailed {
575 reason: "test".into(),
576 }),
577 Box::new(CorrectionError::HmacMismatch { index: 3 }),
578 Box::new(StegoError::NoPayloadFound),
579 Box::new(MediaError::UnsupportedFormat {
580 extension: "xyz".into(),
581 }),
582 Box::new(PdfError::ParseFailed {
583 reason: "test".into(),
584 }),
585 Box::new(DistributionError::InsufficientCovers { needed: 3, got: 1 }),
586 Box::new(ReconstructionError::InsufficientCovers { needed: 3, got: 1 }),
587 Box::new(AnalysisError::ComputationFailed {
588 reason: "test".into(),
589 }),
590 Box::new(ArchiveError::PackFailed {
591 reason: "test".into(),
592 }),
593 Box::new(AdaptiveError::BudgetNotMet {
594 target_db: 40.0,
595 achieved_db: 35.5,
596 }),
597 Box::new(DeniableError::InsufficientCapacity),
598 Box::new(OpsecError::PipelineError {
599 reason: "test".into(),
600 }),
601 Box::new(CanaryError::NoCovers),
602 Box::new(DeadDropError::UnsupportedPlatform {
603 reason: "test".into(),
604 }),
605 Box::new(TimeLockError::NotYetSolvable {
606 unlock_at: "2030-01-01T00:00:00Z".into(),
607 }),
608 Box::new(ScrubberError::InvalidUtf8 {
609 reason: "test".into(),
610 }),
611 Box::new(CorpusError::NoSuitableCover {
612 payload_bytes: 1024,
613 }),
614 ];
615 for e in &errors {
616 // Must not panic and must produce a non-empty string.
617 assert!(!e.to_string().is_empty(), "empty display for {e:?}");
618 }
619 }
620}