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 #[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 #[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 #[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 #[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}