stenoxide-core 3.7.4

Adaptive LSB steganography engine with HILL cost functions, STC embedding and an Argon2id + XChaCha20-Poly1305 cryptographic pipeline
Documentation
//! The single error type of the pipeline layer.
//!
//! Every layer below reports its own failures in its own vocabulary, and none
//! of them knows the others exist. [`PipelineError`] is where those vocabularies
//! meet: one variant per lower layer, each holding the original error rather
//! than a message rendered from it, so a caller that wants to react to a
//! specific condition — a payload that does not fit, a container without
//! texture — can still match on it after it has crossed the pipeline boundary.
//!
//! The [`std::error::Error::source`] chain is wired for every variant, which is
//! what lets a front-end print the whole causal chain without this module
//! having to flatten it into a string.

use std::fmt;

use crate::cost::hill::CostError;
use crate::crypto::aead::CryptoError;
use crate::crypto::expand::ExpandError;
use crate::crypto::kdf::KdfError;
use crate::image_io::phash::PHashError;
use crate::image_io::validate::ValidationError;
use crate::stego::sizer::SizerError;
use crate::stego::stc::StcError;

/// Failure while writing the stego image to disk.
///
/// The only failure of the pipeline that no lower layer can report: layers 1 to
/// 4 read images and never write them, so there is no error type to reuse here.
#[derive(Debug)]
pub enum OutputError {
    /// The sample buffer did not match the geometry it claims to have.
    ///
    /// Unreachable for a buffer that came out of
    /// [`crate::image_io::validate::load_and_validate`], whose length is fixed
    /// by the decoder. Kept as a variant rather than as an assertion because the
    /// encoder's constructor is fallible and swallowing that would mean writing
    /// a file the caller believes is a valid container.
    MalformedBuffer,
    /// The PNG encoder or the filesystem refused the write.
    EncodingFailed(String),
}

impl fmt::Display for OutputError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            OutputError::MalformedBuffer => write!(
                f,
                "the stego image buffer does not match its own dimensions and cannot be encoded"
            ),
            OutputError::EncodingFailed(message) => {
                write!(f, "failed to write the stego image: {message}")
            }
        }
    }
}

impl std::error::Error for OutputError {}

/// Everything that can go wrong between a plaintext and a stego image.
///
/// The variants follow the order the layers run in, which is also the order in
/// which a caller will meet them.
#[derive(Debug)]
pub enum PipelineError {
    /// The container image failed a validation gate of layer 1.
    Validation(ValidationError),
    /// The container's perceptual hash is not reproducible, or the salt of a
    /// stego image could not be recovered.
    PHash(PHashError),
    /// Argon2id password stretching failed.
    Kdf(KdfError),
    /// HKDF-SHA3-512 expansion of the master key failed.
    Expand(ExpandError),
    /// The cost layer refused the container.
    Cost(CostError),
    /// The payload does not fit in the container.
    Sizer(SizerError),
    /// The Syndrome-Trellis coder refused the operation.
    Stc(StcError),
    /// Compression, encryption or authentication failed.
    Crypto(CryptoError),
    /// The stego image could not be written to disk.
    Output(OutputError),
}

impl fmt::Display for PipelineError {
    /// Delegates to the wrapped error.
    ///
    /// No prefix is added. The lower layers already phrase their messages for a
    /// user — "the message does not fit in this image", "choose a container with
    /// more texture" — and wrapping them in "pipeline error: ..." would only
    /// push the actionable part of the sentence further from the start of the
    /// line.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            PipelineError::Validation(err) => write!(f, "{err}"),
            PipelineError::PHash(err) => write!(f, "{err}"),
            PipelineError::Kdf(err) => write!(f, "{err}"),
            PipelineError::Expand(err) => write!(f, "{err}"),
            PipelineError::Cost(err) => write!(f, "{err}"),
            PipelineError::Sizer(err) => write!(f, "{err}"),
            PipelineError::Stc(err) => write!(f, "{err}"),
            PipelineError::Crypto(err) => write!(f, "{err}"),
            PipelineError::Output(err) => write!(f, "{err}"),
        }
    }
}

impl std::error::Error for PipelineError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            PipelineError::Validation(err) => Some(err),
            PipelineError::PHash(err) => Some(err),
            PipelineError::Kdf(err) => Some(err),
            PipelineError::Expand(err) => Some(err),
            PipelineError::Cost(err) => Some(err),
            PipelineError::Sizer(err) => Some(err),
            PipelineError::Stc(err) => Some(err),
            PipelineError::Crypto(err) => Some(err),
            PipelineError::Output(err) => Some(err),
        }
    }
}

impl From<ValidationError> for PipelineError {
    fn from(err: ValidationError) -> Self {
        PipelineError::Validation(err)
    }
}

impl From<PHashError> for PipelineError {
    fn from(err: PHashError) -> Self {
        PipelineError::PHash(err)
    }
}

impl From<KdfError> for PipelineError {
    fn from(err: KdfError) -> Self {
        PipelineError::Kdf(err)
    }
}

impl From<ExpandError> for PipelineError {
    fn from(err: ExpandError) -> Self {
        PipelineError::Expand(err)
    }
}

impl From<CostError> for PipelineError {
    fn from(err: CostError) -> Self {
        PipelineError::Cost(err)
    }
}

impl From<SizerError> for PipelineError {
    fn from(err: SizerError) -> Self {
        PipelineError::Sizer(err)
    }
}

impl From<StcError> for PipelineError {
    fn from(err: StcError) -> Self {
        PipelineError::Stc(err)
    }
}

impl From<CryptoError> for PipelineError {
    fn from(err: CryptoError) -> Self {
        PipelineError::Crypto(err)
    }
}

impl From<OutputError> for PipelineError {
    fn from(err: OutputError) -> Self {
        PipelineError::Output(err)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::crypto::aead::AEADError;

    /// One error of every lower layer, already lifted into this one.
    ///
    /// Built through the `From` conversions rather than by naming the variants,
    /// so the list also pins that every layer can reach the pipeline with the
    /// `?` operator.
    fn one_of_each() -> Vec<PipelineError> {
        vec![
            ValidationError::NotPng.into(),
            PHashError::RecoveryFailed.into(),
            KdfError::EmptyPassword.into(),
            ExpandError::HkdfError("output too long".to_owned()).into(),
            CostError::InsufficientGlobalTexture.into(),
            SizerError::PayloadTooLarge {
                payload: 100,
                available: 10,
                deficit: 90,
            }
            .into(),
            StcError::InvalidCostMap.into(),
            CryptoError::AEADError(AEADError::AuthenticationFailed).into(),
            OutputError::MalformedBuffer.into(),
        ]
    }

    /// Each conversion lands in the variant named after its layer.
    #[test]
    fn every_layer_lifts_into_its_own_variant() {
        let lifted = one_of_each();

        assert!(matches!(lifted[0], PipelineError::Validation(_)));
        assert!(matches!(lifted[1], PipelineError::PHash(_)));
        assert!(matches!(lifted[2], PipelineError::Kdf(_)));
        assert!(matches!(lifted[3], PipelineError::Expand(_)));
        assert!(matches!(lifted[4], PipelineError::Cost(_)));
        assert!(matches!(lifted[5], PipelineError::Sizer(_)));
        assert!(matches!(lifted[6], PipelineError::Stc(_)));
        assert!(matches!(lifted[7], PipelineError::Crypto(_)));
        assert!(matches!(lifted[8], PipelineError::Output(_)));
    }

    /// The wrapper adds no prefix: the message a user sees is the one the layer
    /// that refused wrote.
    #[test]
    fn the_message_is_the_message_of_the_wrapped_error() {
        assert_eq!(
            PipelineError::from(ValidationError::NotPng).to_string(),
            ValidationError::NotPng.to_string()
        );

        for error in one_of_each() {
            assert!(!error.to_string().is_empty());
        }
    }

    /// Every variant chains to the error it wraps, so a front-end can print the
    /// whole causal chain.
    #[test]
    fn every_variant_names_its_cause() {
        for error in one_of_each() {
            assert!(
                std::error::Error::source(&error).is_some(),
                "no cause behind: {error:?}"
            );
        }
    }

    /// The one failure no lower layer can report explains itself too.
    #[test]
    fn writing_failures_explain_themselves() {
        assert!(OutputError::MalformedBuffer
            .to_string()
            .contains("dimensions"));
        assert!(OutputError::EncodingFailed("disk full".to_owned())
            .to_string()
            .contains("disk full"));
        assert!(std::error::Error::source(&OutputError::MalformedBuffer).is_none());
    }
}