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