Skip to main content

wsi_rs/
error.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4#[derive(Debug, thiserror::Error)]
5#[non_exhaustive]
6pub enum WsiError {
7    #[error("read cancelled")]
8    Cancelled,
9    #[error("unsupported format: {0}")]
10    UnsupportedFormat(String),
11    #[error("TIFF error in {path}: {message}")]
12    Tiff { path: PathBuf, message: String },
13    #[error("JPEG decode error: {0}")]
14    Jpeg(String),
15    #[error("JPEG2000 decode error: {0}")]
16    Jp2k(String),
17    #[error("XML parse error: {0}")]
18    Xml(String),
19    #[error("invalid slide {path}: {message}")]
20    InvalidSlide { path: PathBuf, message: String },
21    #[error("tile read failed at ({col}, {row}) level {level}: {reason}")]
22    TileRead {
23        col: i64,
24        row: i64,
25        level: u32,
26        reason: String,
27    },
28    #[error("I/O error: {0}")]
29    Io(#[from] std::io::Error),
30    #[error("I/O error at {path}: {source}")]
31    IoWithPath {
32        #[source]
33        source: Arc<std::io::Error>,
34        path: PathBuf,
35    },
36
37    /// An input or requested output exceeded a checked byte budget.
38    #[error(
39        "resource limit exceeded for {resource}: requested {requested} bytes, limit {limit} bytes"
40    )]
41    ResourceLimit {
42        resource: &'static str,
43        requested: u64,
44        limit: u64,
45    },
46
47    // --- New variants for multi-dimensional engine ---
48    #[error("scene index {index} out of range (dataset has {count} scenes)")]
49    SceneOutOfRange { index: usize, count: usize },
50
51    #[error("series index {index} out of range (scene has {count} series)")]
52    SeriesOutOfRange { index: usize, count: usize },
53
54    #[error("level {level} out of range (series has {count} levels)")]
55    LevelOutOfRange { level: u32, count: u32 },
56
57    #[error("plane axis {axis} value {value} exceeds max {max}")]
58    PlaneOutOfRange { axis: String, value: u32, max: u32 },
59
60    #[error("associated image not found: {0}")]
61    AssociatedImageNotFound(String),
62
63    #[error("display conversion error: {0}")]
64    DisplayConversion(String),
65
66    #[error("backend contract violation in {context}: expected {expected} results, got {actual}")]
67    BackendContract {
68        context: &'static str,
69        expected: usize,
70        actual: usize,
71    },
72
73    /// Codec-layer error from a j2k backend.
74    #[error("codec error in {codec}: {source}")]
75    Codec {
76        codec: &'static str,
77        #[source]
78        source: Box<dyn std::error::Error + Send + Sync>,
79    },
80
81    /// Operation is intentionally unsupported on this path.
82    #[error("unsupported: {reason}")]
83    Unsupported { reason: String },
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn error_display_formats() {
92        let err = WsiError::Tiff {
93            path: "/tmp/test.svs".into(),
94            message: "bad IFD".into(),
95        };
96        assert!(err.to_string().contains("test.svs"));
97        assert!(err.to_string().contains("bad IFD"));
98    }
99
100    #[test]
101    fn io_error_converts() {
102        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "gone");
103        let wsi_err: WsiError = io_err.into();
104        assert!(matches!(wsi_err, WsiError::Io(_)));
105    }
106
107    #[test]
108    fn scene_out_of_range_display() {
109        let err = WsiError::SceneOutOfRange { index: 2, count: 1 };
110        assert!(err.to_string().contains("2"));
111        assert!(err.to_string().contains("1"));
112    }
113
114    #[test]
115    fn series_out_of_range_display() {
116        let err = WsiError::SeriesOutOfRange { index: 3, count: 2 };
117        assert!(err.to_string().contains("3"));
118    }
119
120    #[test]
121    fn plane_out_of_range_display() {
122        let err = WsiError::PlaneOutOfRange {
123            axis: "z".into(),
124            value: 5,
125            max: 3,
126        };
127        assert!(err.to_string().contains("z"));
128        assert!(err.to_string().contains("5"));
129    }
130
131    #[test]
132    fn level_out_of_range_display() {
133        let err = WsiError::LevelOutOfRange {
134            level: 10,
135            count: 5,
136        };
137        assert!(err.to_string().contains("10"));
138    }
139
140    #[test]
141    fn associated_image_not_found_display() {
142        let err = WsiError::AssociatedImageNotFound("label".into());
143        assert!(err.to_string().contains("label"));
144    }
145
146    #[test]
147    fn display_conversion_display() {
148        let err = WsiError::DisplayConversion("non-uint8 requires windowing".into());
149        assert!(err.to_string().contains("windowing"));
150    }
151
152    #[test]
153    fn io_with_path_display() {
154        let err = WsiError::IoWithPath {
155            source: Arc::new(std::io::Error::new(
156                std::io::ErrorKind::NotFound,
157                "file not found",
158            )),
159            path: "/tmp/slide.svs".into(),
160        };
161        let msg = err.to_string();
162        assert!(msg.contains("/tmp/slide.svs"), "got: {msg}");
163        assert!(msg.contains("file not found"), "got: {msg}");
164    }
165
166    #[test]
167    fn resource_limit_preserves_typed_byte_counts() {
168        let err = WsiError::ResourceLimit {
169            resource: "compressed DICOM frame",
170            requested: 513,
171            limit: 512,
172        };
173        assert!(err.to_string().contains("compressed DICOM frame"));
174        assert!(matches!(
175            err,
176            WsiError::ResourceLimit {
177                requested: 513,
178                limit: 512,
179                ..
180            }
181        ));
182    }
183
184    #[test]
185    fn codec_display_includes_codec_and_source() {
186        let inner: Box<dyn std::error::Error + Send + Sync> = "boom".into();
187        let err = WsiError::Codec {
188            codec: "jpeg",
189            source: inner,
190        };
191        let msg = err.to_string();
192        assert!(msg.contains("jpeg"), "got: {msg}");
193        assert!(msg.contains("boom"), "got: {msg}");
194    }
195
196    #[test]
197    fn codec_pattern_match_round_trips() {
198        let err = WsiError::Codec {
199            codec: "j2k",
200            source: "decode failed".into(),
201        };
202        match err {
203            WsiError::Codec { codec, source: _ } => assert_eq!(codec, "j2k"),
204            other => panic!("expected Codec, got {other:?}"),
205        }
206    }
207
208    #[test]
209    fn unsupported_display() {
210        let err = WsiError::Unsupported {
211            reason: "device backend unavailable".into(),
212        };
213        assert!(err.to_string().contains("device backend unavailable"));
214    }
215}