Files
DarkRoom/core/dr-decode/src/error.rs
T
dtourolle ed4460cb9c Tag three requirements the code already meets
R5 says in its own note that zoom_resolution.rs establishes it as a
pixel equality; that file was tagged FR-DSP-5 alone. FR-DEV-19's three
sub-clauses carry eighty-three tags between them while the parent had
none; MaskLayer, which is the thing they edit, now carries it. And
NFR-R3 — a crash in decode does not take down the application, the
image is marked failed — is exactly what the decoder's panic guard and
the face sweep's unreadable mark do, tagged FR-RAW-4 and NFR-SEC-1 and
not the clause that asked for them.
2026-09-19 12:25:04 +02:00

113 lines
4.1 KiB
Rust

/// 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),
}
/// Run a decoder call, and return a panic inside it as an error.
///
/// TRACES: FR-RAW-4 | NFR-SEC-1 | NFR-R3
/// rawler `panic!`s on some malformed input rather than returning `Err` — a
/// DNG whose IFD claims a >50000 px image, for one, which is in the reference
/// library. A panic on a worker thread ends the thread: the face sweep that
/// met that file stopped 13 seconds in, three sweeps running, with "17301
/// image(s) to index" as the last word and nothing to say why. FR-RAW-4's
/// rule — a malformed file must not abort a batch — is this crate's to keep
/// whatever the library beneath it does, so every entry point that calls into
/// rawler runs through here, and a file that panics the decoder is one failed
/// file like any other.
///
/// The crash hook still records the panic, because it runs before unwinding
/// reaches this frame; that is right — it is a real defect in a dependency
/// and the record is how it gets reported upstream — and a repeat is the same
/// file being met again rather than a new fault.
pub(crate) fn guarded<T>(
what: &'static str,
f: impl FnOnce() -> Result<T, DecodeError>,
) -> Result<T, DecodeError> {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
Ok(result) => result,
Err(payload) => {
let msg = payload
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "no message".to_string());
Err(DecodeError::Decode(format!(
"{what}: the decoder panicked on this file: {msg}"
)))
}
}
}
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());
}
#[test]
fn a_panic_in_the_decoder_is_an_error_and_the_thread_survives() {
// The property the face sweep relies on: one file that panics rawler
// is one failed file, not the end of the pass. The message travels,
// because "decode failed" alone sends the reader to the crash log.
let err = guarded("decode", || -> Result<(), DecodeError> {
panic!("rawler: surely there's no such thing as a {}MP image!", 600)
})
.unwrap_err();
let text = err.to_string();
assert!(text.contains("panicked"), "{text}");
assert!(text.contains("600MP"), "{text}");
assert!(!err.has_fallback(), "a panic is not a missing preview");
}
#[test]
fn a_result_passes_through_untouched() {
assert_eq!(guarded("decode", || Ok::<_, DecodeError>(7)).unwrap(), 7);
assert!(matches!(
guarded("decode", || Err::<(), _>(DecodeError::NoPreview)),
Err(DecodeError::NoPreview)
));
}
}