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 /// Failed to bind or load the pdfium shared library at runtime.
226 #[error("pdfium library binding failed: {reason}")]
227 BindFailed {
228 /// Details about which binding attempts were tried and why they failed.
229 reason: String,
230 },
231}
232
233// ─── DistributionError ────────────────────────────────────────────────────────
234
235/// Errors produced by the distribution bounded context.
236#[derive(Debug, Error)]
237pub enum DistributionError {
238 /// Fewer covers were provided than the distribution pattern requires.
239 #[error("insufficient covers: need {needed}, got {got}")]
240 InsufficientCovers {
241 /// Minimum number of covers the pattern needs.
242 needed: usize,
243 /// Number of covers actually provided.
244 got: usize,
245 },
246 /// An embedding step failed during distribution.
247 #[error("embed failed on cover {index}: {source}")]
248 EmbedFailed {
249 /// Zero-based cover index at which embedding failed.
250 index: usize,
251 /// The underlying stego error.
252 #[source]
253 source: StegoError,
254 },
255 /// Error-correction encoding failed during shard production.
256 #[error("error correction failed: {source}")]
257 CorrectionFailed {
258 /// The underlying correction error.
259 #[source]
260 source: CorrectionError,
261 },
262}
263
264// ─── ReconstructionError ──────────────────────────────────────────────────────
265
266/// Errors produced by the reconstruction bounded context.
267#[derive(Debug, Error)]
268pub enum ReconstructionError {
269 /// Not enough valid stego covers were provided.
270 #[error("insufficient covers for reconstruction: need {needed}, got {got}")]
271 InsufficientCovers {
272 /// Minimum required covers.
273 needed: usize,
274 /// Covers actually provided.
275 got: usize,
276 },
277 /// Payload extraction from a stego cover failed.
278 #[error("extraction failed on cover {index}: {source}")]
279 ExtractionFailed {
280 /// Zero-based cover index at which extraction failed.
281 index: usize,
282 /// The underlying stego error.
283 #[source]
284 source: StegoError,
285 },
286 /// Error-correction decoding failed.
287 #[error("error correction failed: {source}")]
288 CorrectionFailed {
289 /// The underlying correction error.
290 #[source]
291 source: CorrectionError,
292 },
293 /// Manifest signature verification failed.
294 #[error("manifest verification failed: {reason}")]
295 ManifestVerificationFailed {
296 /// Human-readable reason the manifest failed.
297 reason: String,
298 },
299}
300
301// ─── AnalysisError ────────────────────────────────────────────────────────────
302
303/// Errors produced by the analysis bounded context.
304#[derive(Debug, Error)]
305pub enum AnalysisError {
306 /// The cover medium type is incompatible with the requested technique.
307 #[error("unsupported cover type for analysis: {reason}")]
308 UnsupportedCoverType {
309 /// Human-readable description of the incompatibility.
310 reason: String,
311 },
312 /// Statistical computation failed (e.g. divide-by-zero on empty cover).
313 #[error("statistical computation failed: {reason}")]
314 ComputationFailed {
315 /// Human-readable reason for the failure.
316 reason: String,
317 },
318}
319
320// ─── ArchiveError ─────────────────────────────────────────────────────────────
321
322/// Errors produced by the archive bounded context.
323#[derive(Debug, Error)]
324pub enum ArchiveError {
325 /// Packing files into the archive failed.
326 #[error("archive pack error: {reason}")]
327 PackFailed {
328 /// Human-readable reason for the pack failure.
329 reason: String,
330 },
331 /// Unpacking the archive failed.
332 #[error("archive unpack error: {reason}")]
333 UnpackFailed {
334 /// Human-readable reason for the unpack failure.
335 reason: String,
336 },
337 /// The archive format is not supported.
338 #[error("unsupported archive format: {reason}")]
339 UnsupportedFormat {
340 /// Human-readable description of the unsupported format.
341 reason: String,
342 },
343}
344
345// ─── AdaptiveError ────────────────────────────────────────────────────────────
346
347/// Errors produced by the adaptive embedding bounded context.
348#[derive(Debug, Error)]
349pub enum AdaptiveError {
350 /// The optimiser failed to find an embedding that meets the detectability budget.
351 #[error(
352 "could not meet detectability budget of {target_db:.2} dB: best was {achieved_db:.2} dB"
353 )]
354 BudgetNotMet {
355 /// Target detectability ceiling in decibels.
356 target_db: f64,
357 /// Best detectability achieved in decibels.
358 achieved_db: f64,
359 },
360 /// Camera model fingerprint matching failed.
361 #[error("profile matching failed: {reason}")]
362 ProfileMatchFailed {
363 /// Human-readable reason for the failure.
364 reason: String,
365 },
366 /// Compression simulation failed.
367 #[error("compression simulation failed: {reason}")]
368 CompressionSimFailed {
369 /// Human-readable reason for the failure.
370 reason: String,
371 },
372 /// The underlying stego operation failed.
373 #[error("stego error during adaptive optimisation: {source}")]
374 StegoFailed {
375 /// The underlying stego error.
376 #[source]
377 source: StegoError,
378 },
379 /// The distributor returned a different number of covers than were supplied.
380 #[error("distribution cover count mismatch: got {got}, expected {expected}")]
381 DistributionCountMismatch {
382 /// Number of covers returned by the distributor.
383 got: usize,
384 /// Number of covers originally supplied.
385 expected: usize,
386 },
387}
388
389// ─── DeniableError ────────────────────────────────────────────────────────────
390
391/// Errors produced by the deniable steganography bounded context.
392#[derive(Debug, Error)]
393pub enum DeniableError {
394 /// The cover cannot hold both the real and decoy payload simultaneously.
395 #[error("cover capacity too small for dual-payload embedding")]
396 InsufficientCapacity,
397 /// Embedding one of the payloads failed.
398 #[error("dual embed failed: {reason}")]
399 EmbedFailed {
400 /// Human-readable reason for the failure.
401 reason: String,
402 },
403 /// Extraction with the provided key failed.
404 #[error("extraction failed for provided key: {reason}")]
405 ExtractionFailed {
406 /// Human-readable reason for the failure.
407 reason: String,
408 },
409}
410
411// ─── OpsecError ───────────────────────────────────────────────────────────────
412
413/// Errors produced by the operational security bounded context.
414#[derive(Debug, Error)]
415pub enum OpsecError {
416 /// A wipe step failed but execution continued.
417 #[error("wipe step failed for path {path}: {reason}")]
418 WipeStepFailed {
419 /// Path that could not be wiped.
420 path: String,
421 /// Human-readable reason for the failure.
422 reason: String,
423 },
424 /// The in-memory pipeline produced corrupt output.
425 #[error("amnesiac pipeline error: {reason}")]
426 PipelineError {
427 /// Human-readable reason for the failure.
428 reason: String,
429 },
430 /// Forensic watermark embedding or identification failed.
431 #[error("watermark error: {reason}")]
432 WatermarkError {
433 /// Human-readable reason for the failure.
434 reason: String,
435 },
436 /// Geographic manifest validation failed.
437 #[error("geographic manifest error: {reason}")]
438 ManifestError {
439 /// Human-readable reason for the failure.
440 reason: String,
441 },
442}
443
444// ─── CanaryError ──────────────────────────────────────────────────────────────
445
446/// Errors produced by the canary bounded context.
447#[derive(Debug, Error)]
448pub enum CanaryError {
449 /// No covers were provided to embed the canary shard into.
450 #[error("no covers provided for canary embedding")]
451 NoCovers,
452 /// Embedding the canary shard failed.
453 #[error("canary embed failed: {source}")]
454 EmbedFailed {
455 /// The underlying stego error.
456 #[source]
457 source: StegoError,
458 },
459}
460
461// ─── DeadDropError ────────────────────────────────────────────────────────────
462
463/// Errors produced by the dead drop bounded context.
464#[derive(Debug, Error)]
465pub enum DeadDropError {
466 /// The platform profile is unknown or unsupported.
467 #[error("unsupported platform for dead drop: {reason}")]
468 UnsupportedPlatform {
469 /// Human-readable description of the issue.
470 reason: String,
471 },
472 /// Encoding for the platform failed.
473 #[error("dead drop encode failed: {reason}")]
474 EncodeFailed {
475 /// Human-readable reason for the failure.
476 reason: String,
477 },
478}
479
480// ─── TimeLockError ────────────────────────────────────────────────────────────
481
482/// Errors produced by the time-lock bounded context.
483#[derive(Debug, Error)]
484pub enum TimeLockError {
485 /// The puzzle is not yet solvable (unlock time has not been reached).
486 #[error("time-lock puzzle not yet solvable; unlock at {unlock_at}")]
487 NotYetSolvable {
488 /// ISO 8601 string of the earliest unlock time.
489 unlock_at: String,
490 },
491 /// The sequential squaring computation overflowed or failed.
492 #[error("puzzle computation failed: {reason}")]
493 ComputationFailed {
494 /// Human-readable reason for the failure.
495 reason: String,
496 },
497 /// Decryption of the time-locked ciphertext failed.
498 #[error("time-lock decrypt failed: {source}")]
499 DecryptFailed {
500 /// The underlying crypto error.
501 #[source]
502 source: CryptoError,
503 },
504}
505
506// ─── ScrubberError ────────────────────────────────────────────────────────────
507
508/// Errors produced by the stylometric scrubber bounded context.
509#[derive(Debug, Error)]
510pub enum ScrubberError {
511 /// The input text is not valid UTF-8.
512 #[error("input is not valid UTF-8: {reason}")]
513 InvalidUtf8 {
514 /// Human-readable reason for the failure.
515 reason: String,
516 },
517 /// Scrubbing failed to satisfy the target stylometric profile.
518 #[error("could not satisfy stylo profile: {reason}")]
519 ProfileNotSatisfied {
520 /// Human-readable reason the profile could not be satisfied.
521 reason: String,
522 },
523}
524
525// ─── CorpusError ──────────────────────────────────────────────────────────────
526
527/// Errors produced by the corpus steganography bounded context.
528#[derive(Debug, Error)]
529pub enum CorpusError {
530 /// No suitable corpus entry was found for the given payload.
531 #[error("no suitable corpus cover found for payload of {payload_bytes} bytes")]
532 NoSuitableCover {
533 /// Size of the payload in bytes.
534 payload_bytes: u64,
535 },
536 /// The corpus index file is missing or corrupt.
537 #[error("corpus index error: {reason}")]
538 IndexError {
539 /// Human-readable description of the index problem.
540 reason: String,
541 },
542 /// A file could not be added to the corpus index.
543 #[error("corpus add failed for path {path}: {reason}")]
544 AddFailed {
545 /// The path that could not be indexed.
546 path: String,
547 /// Human-readable reason for the failure.
548 reason: String,
549 },
550}
551
552// ─── Tests ────────────────────────────────────────────────────────────────────
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn crypto_error_display_does_not_panic() {
560 let e = CryptoError::InvalidKeyLength {
561 expected: 32,
562 got: 16,
563 };
564 assert!(e.to_string().contains("32"));
565 }
566
567 #[test]
568 fn correction_error_display_does_not_panic() {
569 let e = CorrectionError::InsufficientShards {
570 needed: 5,
571 available: 2,
572 };
573 assert!(e.to_string().contains('5'));
574 }
575
576 #[test]
577 fn stego_error_display_does_not_panic() {
578 let e = StegoError::PayloadTooLarge {
579 needed: 1024,
580 available: 512,
581 };
582 assert!(e.to_string().contains("1024"));
583 }
584
585 #[test]
586 fn all_error_variants_display_without_panic() {
587 let errors: Vec<Box<dyn std::error::Error>> = vec![
588 Box::new(CryptoError::KeyGenFailed {
589 reason: "test".into(),
590 }),
591 Box::new(CorrectionError::HmacMismatch { index: 3 }),
592 Box::new(StegoError::NoPayloadFound),
593 Box::new(MediaError::UnsupportedFormat {
594 extension: "xyz".into(),
595 }),
596 Box::new(PdfError::ParseFailed {
597 reason: "test".into(),
598 }),
599 Box::new(DistributionError::InsufficientCovers { needed: 3, got: 1 }),
600 Box::new(ReconstructionError::InsufficientCovers { needed: 3, got: 1 }),
601 Box::new(AnalysisError::ComputationFailed {
602 reason: "test".into(),
603 }),
604 Box::new(ArchiveError::PackFailed {
605 reason: "test".into(),
606 }),
607 Box::new(AdaptiveError::BudgetNotMet {
608 target_db: 40.0,
609 achieved_db: 35.5,
610 }),
611 Box::new(DeniableError::InsufficientCapacity),
612 Box::new(OpsecError::PipelineError {
613 reason: "test".into(),
614 }),
615 Box::new(CanaryError::NoCovers),
616 Box::new(DeadDropError::UnsupportedPlatform {
617 reason: "test".into(),
618 }),
619 Box::new(TimeLockError::NotYetSolvable {
620 unlock_at: "2030-01-01T00:00:00Z".into(),
621 }),
622 Box::new(ScrubberError::InvalidUtf8 {
623 reason: "test".into(),
624 }),
625 Box::new(CorpusError::NoSuitableCover {
626 payload_bytes: 1024,
627 }),
628 ];
629 for e in &errors {
630 // Must not panic and must produce a non-empty string.
631 assert!(!e.to_string().is_empty(), "empty display for {e:?}");
632 }
633 }
634}