Skip to main content

stenoxide_core/pipeline/
error.rs

1//! The single error type of the pipeline layer.
2//!
3//! Every layer below reports its own failures in its own vocabulary, and none
4//! of them knows the others exist. [`PipelineError`] is where those vocabularies
5//! meet: one variant per lower layer, each holding the original error rather
6//! than a message rendered from it, so a caller that wants to react to a
7//! specific condition — a payload that does not fit, a container without
8//! texture — can still match on it after it has crossed the pipeline boundary.
9//!
10//! The [`std::error::Error::source`] chain is wired for every variant, which is
11//! what lets a front-end print the whole causal chain without this module
12//! having to flatten it into a string.
13
14use std::fmt;
15
16use crate::cost::hill::CostError;
17use crate::crypto::aead::CryptoError;
18use crate::crypto::expand::ExpandError;
19use crate::crypto::kdf::KdfError;
20use crate::image_io::phash::PHashError;
21use crate::image_io::validate::ValidationError;
22use crate::stego::sizer::SizerError;
23use crate::stego::stc::StcError;
24
25/// Failure while writing the stego image to disk.
26///
27/// The only failure of the pipeline that no lower layer can report: layers 1 to
28/// 4 read images and never write them, so there is no error type to reuse here.
29#[derive(Debug)]
30pub enum OutputError {
31    /// The sample buffer did not match the geometry it claims to have.
32    ///
33    /// Unreachable for a buffer that came out of
34    /// [`crate::image_io::validate::load_and_validate`], whose length is fixed
35    /// by the decoder. Kept as a variant rather than as an assertion because the
36    /// encoder's constructor is fallible and swallowing that would mean writing
37    /// a file the caller believes is a valid container.
38    MalformedBuffer,
39    /// The PNG encoder or the filesystem refused the write.
40    EncodingFailed(String),
41}
42
43impl fmt::Display for OutputError {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            OutputError::MalformedBuffer => write!(
47                f,
48                "the stego image buffer does not match its own dimensions and cannot be encoded"
49            ),
50            OutputError::EncodingFailed(message) => {
51                write!(f, "failed to write the stego image: {message}")
52            }
53        }
54    }
55}
56
57impl std::error::Error for OutputError {}
58
59/// Everything that can go wrong between a plaintext and a stego image.
60///
61/// The variants follow the order the layers run in, which is also the order in
62/// which a caller will meet them.
63#[derive(Debug)]
64pub enum PipelineError {
65    /// The container image failed a validation gate of layer 1.
66    Validation(ValidationError),
67    /// The container's perceptual hash is not reproducible, or the salt of a
68    /// stego image could not be recovered.
69    PHash(PHashError),
70    /// Argon2id password stretching failed.
71    Kdf(KdfError),
72    /// HKDF-SHA3-512 expansion of the master key failed.
73    Expand(ExpandError),
74    /// The cost layer refused the container.
75    Cost(CostError),
76    /// The payload does not fit in the container.
77    Sizer(SizerError),
78    /// The Syndrome-Trellis coder refused the operation.
79    Stc(StcError),
80    /// Compression, encryption or authentication failed.
81    Crypto(CryptoError),
82    /// The stego image could not be written to disk.
83    Output(OutputError),
84}
85
86impl fmt::Display for PipelineError {
87    /// Delegates to the wrapped error.
88    ///
89    /// No prefix is added. The lower layers already phrase their messages for a
90    /// user — "the message does not fit in this image", "choose a container with
91    /// more texture" — and wrapping them in "pipeline error: ..." would only
92    /// push the actionable part of the sentence further from the start of the
93    /// line.
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        match self {
96            PipelineError::Validation(err) => write!(f, "{err}"),
97            PipelineError::PHash(err) => write!(f, "{err}"),
98            PipelineError::Kdf(err) => write!(f, "{err}"),
99            PipelineError::Expand(err) => write!(f, "{err}"),
100            PipelineError::Cost(err) => write!(f, "{err}"),
101            PipelineError::Sizer(err) => write!(f, "{err}"),
102            PipelineError::Stc(err) => write!(f, "{err}"),
103            PipelineError::Crypto(err) => write!(f, "{err}"),
104            PipelineError::Output(err) => write!(f, "{err}"),
105        }
106    }
107}
108
109impl std::error::Error for PipelineError {
110    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
111        match self {
112            PipelineError::Validation(err) => Some(err),
113            PipelineError::PHash(err) => Some(err),
114            PipelineError::Kdf(err) => Some(err),
115            PipelineError::Expand(err) => Some(err),
116            PipelineError::Cost(err) => Some(err),
117            PipelineError::Sizer(err) => Some(err),
118            PipelineError::Stc(err) => Some(err),
119            PipelineError::Crypto(err) => Some(err),
120            PipelineError::Output(err) => Some(err),
121        }
122    }
123}
124
125impl From<ValidationError> for PipelineError {
126    fn from(err: ValidationError) -> Self {
127        PipelineError::Validation(err)
128    }
129}
130
131impl From<PHashError> for PipelineError {
132    fn from(err: PHashError) -> Self {
133        PipelineError::PHash(err)
134    }
135}
136
137impl From<KdfError> for PipelineError {
138    fn from(err: KdfError) -> Self {
139        PipelineError::Kdf(err)
140    }
141}
142
143impl From<ExpandError> for PipelineError {
144    fn from(err: ExpandError) -> Self {
145        PipelineError::Expand(err)
146    }
147}
148
149impl From<CostError> for PipelineError {
150    fn from(err: CostError) -> Self {
151        PipelineError::Cost(err)
152    }
153}
154
155impl From<SizerError> for PipelineError {
156    fn from(err: SizerError) -> Self {
157        PipelineError::Sizer(err)
158    }
159}
160
161impl From<StcError> for PipelineError {
162    fn from(err: StcError) -> Self {
163        PipelineError::Stc(err)
164    }
165}
166
167impl From<CryptoError> for PipelineError {
168    fn from(err: CryptoError) -> Self {
169        PipelineError::Crypto(err)
170    }
171}
172
173impl From<OutputError> for PipelineError {
174    fn from(err: OutputError) -> Self {
175        PipelineError::Output(err)
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    use crate::crypto::aead::AEADError;
184
185    /// One error of every lower layer, already lifted into this one.
186    ///
187    /// Built through the `From` conversions rather than by naming the variants,
188    /// so the list also pins that every layer can reach the pipeline with the
189    /// `?` operator.
190    fn one_of_each() -> Vec<PipelineError> {
191        vec![
192            ValidationError::NotPng.into(),
193            PHashError::RecoveryFailed.into(),
194            KdfError::EmptyPassword.into(),
195            ExpandError::HkdfError("output too long".to_owned()).into(),
196            CostError::InsufficientGlobalTexture.into(),
197            SizerError::PayloadTooLarge {
198                payload: 100,
199                available: 10,
200                deficit: 90,
201            }
202            .into(),
203            StcError::InvalidCostMap.into(),
204            CryptoError::AEADError(AEADError::AuthenticationFailed).into(),
205            OutputError::MalformedBuffer.into(),
206        ]
207    }
208
209    /// Each conversion lands in the variant named after its layer.
210    #[test]
211    fn every_layer_lifts_into_its_own_variant() {
212        let lifted = one_of_each();
213
214        assert!(matches!(lifted[0], PipelineError::Validation(_)));
215        assert!(matches!(lifted[1], PipelineError::PHash(_)));
216        assert!(matches!(lifted[2], PipelineError::Kdf(_)));
217        assert!(matches!(lifted[3], PipelineError::Expand(_)));
218        assert!(matches!(lifted[4], PipelineError::Cost(_)));
219        assert!(matches!(lifted[5], PipelineError::Sizer(_)));
220        assert!(matches!(lifted[6], PipelineError::Stc(_)));
221        assert!(matches!(lifted[7], PipelineError::Crypto(_)));
222        assert!(matches!(lifted[8], PipelineError::Output(_)));
223    }
224
225    /// The wrapper adds no prefix: the message a user sees is the one the layer
226    /// that refused wrote.
227    #[test]
228    fn the_message_is_the_message_of_the_wrapped_error() {
229        assert_eq!(
230            PipelineError::from(ValidationError::NotPng).to_string(),
231            ValidationError::NotPng.to_string()
232        );
233
234        for error in one_of_each() {
235            assert!(!error.to_string().is_empty());
236        }
237    }
238
239    /// Every variant chains to the error it wraps, so a front-end can print the
240    /// whole causal chain.
241    #[test]
242    fn every_variant_names_its_cause() {
243        for error in one_of_each() {
244            assert!(
245                std::error::Error::source(&error).is_some(),
246                "no cause behind: {error:?}"
247            );
248        }
249    }
250
251    /// The one failure no lower layer can report explains itself too.
252    #[test]
253    fn writing_failures_explain_themselves() {
254        assert!(OutputError::MalformedBuffer
255            .to_string()
256            .contains("dimensions"));
257        assert!(OutputError::EncodingFailed("disk full".to_owned())
258            .to_string()
259            .contains("disk full"));
260        assert!(std::error::Error::source(&OutputError::MalformedBuffer).is_none());
261    }
262}