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 /// The distributor returned a different number of covers than were supplied.
374 #[error("distribution cover count mismatch: got {got}, expected {expected}")]
375 DistributionCountMismatch {
376 /// Number of covers returned by the distributor.
377 got: usize,
378 /// Number of covers originally supplied.
379 expected: usize,
380 },
381}
382
383// ─── DeniableError ────────────────────────────────────────────────────────────
384
385/// Errors produced by the deniable steganography bounded context.
386#[derive(Debug, Error)]
387pub enum DeniableError {
388 /// The cover cannot hold both the real and decoy payload simultaneously.
389 #[error("cover capacity too small for dual-payload embedding")]
390 InsufficientCapacity,
391 /// Embedding one of the payloads failed.
392 #[error("dual embed failed: {reason}")]
393 EmbedFailed {
394 /// Human-readable reason for the failure.
395 reason: String,
396 },
397 /// Extraction with the provided key failed.
398 #[error("extraction failed for provided key: {reason}")]
399 ExtractionFailed {
400 /// Human-readable reason for the failure.
401 reason: String,
402 },
403}
404
405// ─── OpsecError ───────────────────────────────────────────────────────────────
406
407/// Errors produced by the operational security bounded context.
408#[derive(Debug, Error)]
409pub enum OpsecError {
410 /// A wipe step failed but execution continued.
411 #[error("wipe step failed for path {path}: {reason}")]
412 WipeStepFailed {
413 /// Path that could not be wiped.
414 path: String,
415 /// Human-readable reason for the failure.
416 reason: String,
417 },
418 /// The in-memory pipeline produced corrupt output.
419 #[error("amnesiac pipeline error: {reason}")]
420 PipelineError {
421 /// Human-readable reason for the failure.
422 reason: String,
423 },
424 /// Forensic watermark embedding or identification failed.
425 #[error("watermark error: {reason}")]
426 WatermarkError {
427 /// Human-readable reason for the failure.
428 reason: String,
429 },
430 /// Geographic manifest validation failed.
431 #[error("geographic manifest error: {reason}")]
432 ManifestError {
433 /// Human-readable reason for the failure.
434 reason: String,
435 },
436}
437
438// ─── CanaryError ──────────────────────────────────────────────────────────────
439
440/// Errors produced by the canary bounded context.
441#[derive(Debug, Error)]
442pub enum CanaryError {
443 /// No covers were provided to embed the canary shard into.
444 #[error("no covers provided for canary embedding")]
445 NoCovers,
446 /// Embedding the canary shard failed.
447 #[error("canary embed failed: {source}")]
448 EmbedFailed {
449 /// The underlying stego error.
450 #[source]
451 source: StegoError,
452 },
453}
454
455// ─── DeadDropError ────────────────────────────────────────────────────────────
456
457/// Errors produced by the dead drop bounded context.
458#[derive(Debug, Error)]
459pub enum DeadDropError {
460 /// The platform profile is unknown or unsupported.
461 #[error("unsupported platform for dead drop: {reason}")]
462 UnsupportedPlatform {
463 /// Human-readable description of the issue.
464 reason: String,
465 },
466 /// Encoding for the platform failed.
467 #[error("dead drop encode failed: {reason}")]
468 EncodeFailed {
469 /// Human-readable reason for the failure.
470 reason: String,
471 },
472}
473
474// ─── TimeLockError ────────────────────────────────────────────────────────────
475
476/// Errors produced by the time-lock bounded context.
477#[derive(Debug, Error)]
478pub enum TimeLockError {
479 /// The puzzle is not yet solvable (unlock time has not been reached).
480 #[error("time-lock puzzle not yet solvable; unlock at {unlock_at}")]
481 NotYetSolvable {
482 /// ISO 8601 string of the earliest unlock time.
483 unlock_at: String,
484 },
485 /// The sequential squaring computation overflowed or failed.
486 #[error("puzzle computation failed: {reason}")]
487 ComputationFailed {
488 /// Human-readable reason for the failure.
489 reason: String,
490 },
491 /// Decryption of the time-locked ciphertext failed.
492 #[error("time-lock decrypt failed: {source}")]
493 DecryptFailed {
494 /// The underlying crypto error.
495 #[source]
496 source: CryptoError,
497 },
498}
499
500// ─── ScrubberError ────────────────────────────────────────────────────────────
501
502/// Errors produced by the stylometric scrubber bounded context.
503#[derive(Debug, Error)]
504pub enum ScrubberError {
505 /// The input text is not valid UTF-8.
506 #[error("input is not valid UTF-8: {reason}")]
507 InvalidUtf8 {
508 /// Human-readable reason for the failure.
509 reason: String,
510 },
511 /// Scrubbing failed to satisfy the target stylometric profile.
512 #[error("could not satisfy stylo profile: {reason}")]
513 ProfileNotSatisfied {
514 /// Human-readable reason the profile could not be satisfied.
515 reason: String,
516 },
517}
518
519// ─── CorpusError ──────────────────────────────────────────────────────────────
520
521/// Errors produced by the corpus steganography bounded context.
522#[derive(Debug, Error)]
523pub enum CorpusError {
524 /// No suitable corpus entry was found for the given payload.
525 #[error("no suitable corpus cover found for payload of {payload_bytes} bytes")]
526 NoSuitableCover {
527 /// Size of the payload in bytes.
528 payload_bytes: u64,
529 },
530 /// The corpus index file is missing or corrupt.
531 #[error("corpus index error: {reason}")]
532 IndexError {
533 /// Human-readable description of the index problem.
534 reason: String,
535 },
536 /// A file could not be added to the corpus index.
537 #[error("corpus add failed for path {path}: {reason}")]
538 AddFailed {
539 /// The path that could not be indexed.
540 path: String,
541 /// Human-readable reason for the failure.
542 reason: String,
543 },
544}
545
546// ─── Tests ────────────────────────────────────────────────────────────────────
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn crypto_error_display_does_not_panic() {
554 let e = CryptoError::InvalidKeyLength {
555 expected: 32,
556 got: 16,
557 };
558 assert!(e.to_string().contains("32"));
559 }
560
561 #[test]
562 fn correction_error_display_does_not_panic() {
563 let e = CorrectionError::InsufficientShards {
564 needed: 5,
565 available: 2,
566 };
567 assert!(e.to_string().contains('5'));
568 }
569
570 #[test]
571 fn stego_error_display_does_not_panic() {
572 let e = StegoError::PayloadTooLarge {
573 needed: 1024,
574 available: 512,
575 };
576 assert!(e.to_string().contains("1024"));
577 }
578
579 #[test]
580 fn all_error_variants_display_without_panic() {
581 let errors: Vec<Box<dyn std::error::Error>> = vec![
582 Box::new(CryptoError::KeyGenFailed {
583 reason: "test".into(),
584 }),
585 Box::new(CorrectionError::HmacMismatch { index: 3 }),
586 Box::new(StegoError::NoPayloadFound),
587 Box::new(MediaError::UnsupportedFormat {
588 extension: "xyz".into(),
589 }),
590 Box::new(PdfError::ParseFailed {
591 reason: "test".into(),
592 }),
593 Box::new(DistributionError::InsufficientCovers { needed: 3, got: 1 }),
594 Box::new(ReconstructionError::InsufficientCovers { needed: 3, got: 1 }),
595 Box::new(AnalysisError::ComputationFailed {
596 reason: "test".into(),
597 }),
598 Box::new(ArchiveError::PackFailed {
599 reason: "test".into(),
600 }),
601 Box::new(AdaptiveError::BudgetNotMet {
602 target_db: 40.0,
603 achieved_db: 35.5,
604 }),
605 Box::new(DeniableError::InsufficientCapacity),
606 Box::new(OpsecError::PipelineError {
607 reason: "test".into(),
608 }),
609 Box::new(CanaryError::NoCovers),
610 Box::new(DeadDropError::UnsupportedPlatform {
611 reason: "test".into(),
612 }),
613 Box::new(TimeLockError::NotYetSolvable {
614 unlock_at: "2030-01-01T00:00:00Z".into(),
615 }),
616 Box::new(ScrubberError::InvalidUtf8 {
617 reason: "test".into(),
618 }),
619 Box::new(CorpusError::NoSuitableCover {
620 payload_bytes: 1024,
621 }),
622 ];
623 for e in &errors {
624 // Must not panic and must produce a non-empty string.
625 assert!(!e.to_string().is_empty(), "empty display for {e:?}");
626 }
627 }
628}