/// TRACES: FR-RAW-4 | NFR-SEC-1 /// Failures from decoding. /// /// Per FR-RAW-4 a malformed file must not abort a batch, so these are always /// returned rather than panicking — and the decode path is the one place /// untrusted input arrives (NFR-SEC-1). #[derive(Debug, thiserror::Error)] pub enum DecodeError { #[error("read failed: {0}")] Read(String), #[error("unsupported or unrecognised format: {0}")] Unsupported(String), #[error("decode failed: {0}")] Decode(String), #[error("metadata unavailable: {0}")] Metadata(String), #[error("no embedded preview in this file")] NoPreview, #[error("embedded preview is corrupt: {0}")] CorruptPreview(String), } impl DecodeError { /// Whether a fallback path might still produce an image. /// /// A missing preview is not a failure to display the file — it means fall /// through to full decode (FR-CULL-2, M-11). pub fn has_fallback(&self) -> bool { matches!( self, DecodeError::NoPreview | DecodeError::CorruptPreview(_) ) } } #[cfg(test)] mod tests { use super::*; #[test] fn preview_failures_fall_through_rather_than_failing() { assert!(DecodeError::NoPreview.has_fallback()); assert!(DecodeError::CorruptPreview("truncated".into()).has_fallback()); // A genuinely unsupported file has nowhere to fall through to. assert!(!DecodeError::Unsupported("unknown".into()).has_fallback()); } }