Skip to main content

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}