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}
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}