Skip to main content

phasm_core/stego/
error.rs

1// Copyright (c) 2026 Christoph Gaffga
2// SPDX-License-Identifier: GPL-3.0-only
3// https://github.com/cgaffga/phasmcore
4
5//! Error types for the steganography pipeline.
6//!
7//! [`StegoError`] covers all failure modes from JPEG parsing through
8//! encryption and frame extraction.
9
10use core::fmt;
11
12/// Errors that can occur during steganographic encoding or decoding.
13#[derive(Debug)]
14pub enum StegoError {
15    /// The cover image could not be parsed as a valid JPEG.
16    InvalidJpeg(crate::jpeg::error::JpegError),
17    /// The image is too small or has too few usable coefficients.
18    ImageTooSmall,
19    /// The image dimensions exceed the maximum allowed (16384px / 200MP).
20    ImageTooLarge,
21    /// The message is too large for the cover image's embedding capacity.
22    MessageTooLarge,
23    /// CRC check failed on the extracted payload frame.
24    FrameCorrupted,
25    /// AES-GCM decryption failed (wrong passphrase or corrupted data).
26    DecryptionFailed,
27    /// The extracted plaintext is not valid UTF-8.
28    InvalidUtf8,
29    /// The cover image has no luminance component.
30    NoLuminanceChannel,
31    /// The operation was cancelled by the user.
32    Cancelled,
33    /// Argon2 key derivation failed (invalid parameters or internal error).
34    KeyDerivationFailed,
35    /// Duplicate passphrase: each shadow layer must use a unique passphrase.
36    DuplicatePassphrase,
37    /// Shadow embedding failed cascade — encoder verification couldn't
38    /// recover the shadow message after escalating through all parity
39    /// tiers `[4, 8, 16, 32, 64, 128]`. Caller can retry with a smaller
40    /// primary message (less propagation), different `gop_size`/quality,
41    /// or a different shadow passphrase.
42    ShadowEmbedFailed,
43    /// The video file is invalid or uses unsupported features.
44    InvalidVideo(String),
45}
46
47impl fmt::Display for StegoError {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            Self::InvalidJpeg(e) => write!(f, "invalid JPEG: {e}"),
51            Self::ImageTooSmall => write!(f, "image too small for embedding"),
52            Self::ImageTooLarge => write!(f, "image too large (max 16384px / 200MP)"),
53            Self::MessageTooLarge => write!(f, "message too large for this image"),
54            Self::FrameCorrupted => write!(f, "payload frame CRC mismatch"),
55            Self::DecryptionFailed => write!(f, "decryption failed (wrong passphrase?)"),
56            Self::InvalidUtf8 => write!(f, "extracted text is not valid UTF-8"),
57            Self::NoLuminanceChannel => write!(f, "image has no luminance channel"),
58            Self::Cancelled => write!(f, "operation cancelled by user"),
59            Self::KeyDerivationFailed => write!(f, "key derivation failed"),
60            Self::DuplicatePassphrase => write!(f, "duplicate passphrase (each layer must use a unique passphrase)"),
61            Self::ShadowEmbedFailed => write!(f, "shadow embed failed: cascade exhausted at parity tier 128 — try a smaller primary message, different gop_size/quality, or different shadow passphrase"),
62            Self::InvalidVideo(s) => write!(f, "invalid video: {s}"),
63        }
64    }
65}
66
67impl std::error::Error for StegoError {
68    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
69        match self {
70            Self::InvalidJpeg(e) => Some(e),
71            _ => None,
72        }
73    }
74}
75
76impl From<crate::jpeg::error::JpegError> for StegoError {
77    fn from(e: crate::jpeg::error::JpegError) -> Self {
78        Self::InvalidJpeg(e)
79    }
80}
81
82#[cfg(feature = "video")]
83impl From<crate::codec::h264::H264Error> for StegoError {
84    fn from(e: crate::codec::h264::H264Error) -> Self {
85        Self::InvalidVideo(e.to_string())
86    }
87}
88
89#[cfg(feature = "video")]
90impl From<crate::codec::mp4::Mp4Error> for StegoError {
91    fn from(e: crate::codec::mp4::Mp4Error) -> Self {
92        Self::InvalidVideo(e.to_string())
93    }
94}