Skip to main content

flashraw_core/
error.rs

1//! Structured error type shared by every FlashRaw crate.
2
3use crate::integrity::{CorruptReason, Truncation};
4use std::fmt;
5
6/// Result alias used throughout FlashRaw.
7pub type Result<T, E = Error> = std::result::Result<T, E>;
8
9/// All errors produced while parsing or decoding a RAW file.
10///
11/// Errors carry enough context (format, offset, what was being parsed) to make a
12/// failure on a corpus file actionable without a debugger.
13#[derive(Debug, thiserror::Error)]
14#[non_exhaustive]
15pub enum Error {
16    /// The bytes do not look like any RAW format FlashRaw understands.
17    #[error("unsupported or unrecognised file format{}", fmt_ctx(.0))]
18    UnsupportedFormat(String),
19
20    /// The container was understood but the payload uses an unsupported compression scheme.
21    #[error("unsupported compression in {format}: {compression}")]
22    UnsupportedCompression {
23        /// Container format name (e.g. `"NEF"`).
24        format: &'static str,
25        /// Description of the compression scheme.
26        compression: String,
27    },
28
29    /// The file is structurally invalid (truncated, bad offsets, corrupt entropy data...).
30    ///
31    /// Truncated sensor data carries a structured [`CorruptReason`] (see
32    /// [`Error::corrupt_reason`] and [`Error::truncation`]).
33    #[error("corrupt file: {what}{}", fmt_offset(.offset, .reason))]
34    #[non_exhaustive]
35    CorruptFile {
36        /// What was being parsed or what was wrong.
37        what: String,
38        /// Byte offset in the input where the problem was found, when known.
39        offset: Option<u64>,
40        /// Structured cause, when known (e.g. which strip is truncated and by how much).
41        reason: Option<Box<CorruptReason>>,
42    },
43
44    /// Metadata required for decoding is missing or nonsensical.
45    #[error("invalid metadata: {0}")]
46    InvalidMetadata(String),
47
48    /// The file format is supported but this camera or mode is not (yet).
49    #[error("unsupported camera: {make} {model}{}", fmt_ctx(.reason))]
50    CameraUnsupported {
51        /// Camera make.
52        make: String,
53        /// Camera model.
54        model: String,
55        /// Why the camera or mode is unsupported (may be empty).
56        reason: String,
57    },
58
59    /// A GPU backend was requested but is not available.
60    #[error("GPU unavailable: {0}")]
61    GpuUnavailable(String),
62
63    /// An allocation would exceed configured limits or failed.
64    #[error("out of memory: {0}")]
65    OutOfMemory(String),
66
67    /// Image dimensions or other values exceed configured safety limits.
68    #[error("limit exceeded: {0}")]
69    LimitExceeded(String),
70
71    /// The requested operation is not possible for this file (e.g. no preview, no tiles).
72    #[error("not available: {0}")]
73    NotAvailable(String),
74
75    /// Invalid caller-provided arguments (bad region, incompatible options...).
76    #[error("invalid argument: {0}")]
77    InvalidArgument(String),
78
79    /// I/O failure while reading input.
80    #[error("I/O error: {0}")]
81    Io(#[from] std::io::Error),
82}
83
84fn fmt_offset(offset: &Option<u64>, reason: &Option<Box<CorruptReason>>) -> String {
85    match (offset, reason) {
86        // Structured reasons state their offsets themselves.
87        (Some(o), None) => format!(" (at offset {o:#x})"),
88        _ => String::new(),
89    }
90}
91
92fn fmt_ctx(s: &str) -> String {
93    if s.is_empty() { String::new() } else { format!(": {s}") }
94}
95
96impl Error {
97    /// Shorthand for a [`Error::CorruptFile`] without an offset.
98    #[cold]
99    pub fn corrupt(what: impl fmt::Display) -> Self {
100        Error::CorruptFile { what: what.to_string(), offset: None, reason: None }
101    }
102
103    /// Shorthand for a [`Error::CorruptFile`] with an offset.
104    #[cold]
105    pub fn corrupt_at(what: impl fmt::Display, offset: u64) -> Self {
106        Error::CorruptFile { what: what.to_string(), offset: Some(offset), reason: None }
107    }
108
109    /// A [`Error::CorruptFile`] for sensor data that extends past the end of the input.
110    #[cold]
111    pub fn truncated(t: Truncation) -> Self {
112        Error::CorruptFile {
113            what: t.to_string(),
114            offset: Some(t.offset),
115            reason: Some(Box::new(CorruptReason::Truncated(t))),
116        }
117    }
118
119    /// A [`Error::CorruptFile`] with a structured reason.
120    #[cold]
121    pub fn corrupt_with(reason: CorruptReason) -> Self {
122        let offset = reason.truncation().map(|t| t.offset);
123        Error::CorruptFile { what: reason.to_string(), offset, reason: Some(Box::new(reason)) }
124    }
125
126    /// Attribute a failure to decode a compressed range to truncation when that range
127    /// (`declared` bytes at `offset`) runs past the end of a `file_len`-byte input: the
128    /// error becomes a structured [`CorruptReason::Truncated`] report. Other errors, and
129    /// ranges inside the file, are returned unchanged. (Compressed streams are only
130    /// known to be short when the decoder runs out of data, so the range is not checked
131    /// up front.)
132    pub fn or_truncated(self, file_len: usize, unit: crate::DataUnit, offset: usize, declared: usize) -> Self {
133        match self {
134            Error::CorruptFile { reason: None, .. } => {
135                match crate::integrity::check_range(file_len, unit, offset, declared) {
136                    Ok(()) => self,
137                    Err(t) => Error::truncated(t),
138                }
139            }
140            e => e,
141        }
142    }
143
144    /// The structured cause of a [`Error::CorruptFile`], when known.
145    pub fn corrupt_reason(&self) -> Option<&CorruptReason> {
146        match self {
147            Error::CorruptFile { reason, .. } => reason.as_deref(),
148            _ => None,
149        }
150    }
151
152    /// Details of truncated sensor data (a [`CorruptReason::Truncated`] corruption).
153    pub fn truncation(&self) -> Option<&Truncation> {
154        self.corrupt_reason().and_then(CorruptReason::truncation)
155    }
156
157    /// Shorthand for an [`Error::InvalidMetadata`].
158    #[cold]
159    pub fn invalid_metadata(what: impl fmt::Display) -> Self {
160        Error::InvalidMetadata(what.to_string())
161    }
162
163    /// Shorthand for an [`Error::UnsupportedCompression`].
164    #[cold]
165    pub fn unsupported_compression(format: &'static str, compression: impl fmt::Display) -> Self {
166        Error::UnsupportedCompression { format, compression: compression.to_string() }
167    }
168
169    /// Short machine-friendly classification, used by the CLI's JSON output and the JS bindings.
170    pub fn kind(&self) -> &'static str {
171        match self {
172            Error::UnsupportedFormat(_) => "UnsupportedFormat",
173            Error::UnsupportedCompression { .. } => "UnsupportedCompression",
174            Error::CorruptFile { .. } => "CorruptFile",
175            Error::InvalidMetadata(_) => "InvalidMetadata",
176            Error::CameraUnsupported { .. } => "CameraUnsupported",
177            Error::GpuUnavailable(_) => "GpuUnavailable",
178            Error::OutOfMemory(_) => "OutOfMemory",
179            Error::LimitExceeded(_) => "LimitExceeded",
180            Error::NotAvailable(_) => "NotAvailable",
181            Error::InvalidArgument(_) => "InvalidArgument",
182            Error::Io(_) => "Io",
183        }
184    }
185}
186
187/// Return early with a [`Error::CorruptFile`].
188#[macro_export]
189macro_rules! bail_corrupt {
190    ($($arg:tt)*) => { return Err($crate::Error::corrupt(format!($($arg)*))) };
191}
192
193/// Ensure a condition holds, otherwise return a [`Error::CorruptFile`].
194#[macro_export]
195macro_rules! ensure_corrupt {
196    ($cond:expr, $($arg:tt)*) => {
197        if !$cond { return Err($crate::Error::corrupt(format!($($arg)*))); }
198    };
199}