dicom_toolkit_core/
error.rs1use std::fmt;
7
8#[derive(Debug, thiserror::Error)]
13pub enum DcmError {
14 #[error("I/O error: {0}")]
17 Io(#[from] std::io::Error),
18
19 #[error("unexpected end of data at offset {offset}")]
21 UnexpectedEof { offset: u64 },
22
23 #[error("invalid DICOM file: {reason}")]
26 InvalidFile { reason: String },
27
28 #[error("invalid tag ({group:04X},{element:04X}): {reason}")]
30 InvalidTag {
31 group: u16,
32 element: u16,
33 reason: String,
34 },
35
36 #[error("VR mismatch for tag ({group:04X},{element:04X}): expected {expected}, found {found}")]
38 VrMismatch {
39 group: u16,
40 element: u16,
41 expected: String,
42 found: String,
43 },
44
45 #[error("invalid value for tag ({group:04X},{element:04X}): {reason}")]
47 InvalidValue {
48 group: u16,
49 element: u16,
50 reason: String,
51 },
52
53 #[error("invalid element length {length} for tag ({group:04X},{element:04X})")]
55 InvalidLength {
56 group: u16,
57 element: u16,
58 length: u64,
59 },
60
61 #[error("unsupported transfer syntax: {uid}")]
64 UnsupportedTransferSyntax { uid: String },
65
66 #[error("no codec available for transfer syntax: {uid}")]
68 NoCodec { uid: String },
69
70 #[error("unknown tag ({group:04X},{element:04X})")]
73 UnknownTag { group: u16, element: u16 },
74
75 #[error("invalid UID: {reason}")]
78 InvalidUid { reason: String },
79
80 #[error("character encoding error: {reason}")]
83 CharsetError { reason: String },
84
85 #[error("association rejected: {reason}")]
88 AssociationRejected { reason: String },
89
90 #[error("association aborted: abort_source={abort_source}, reason={reason}")]
92 AssociationAborted {
93 abort_source: String,
94 reason: String,
95 },
96
97 #[error("DIMSE error: status 0x{status:04X} ({description})")]
99 DimseError { status: u16, description: String },
100
101 #[error("network timeout after {seconds}s")]
103 Timeout { seconds: u64 },
104
105 #[error("no accepted presentation context for SOP class {sop_class_uid}")]
107 NoPresentationContext { sop_class_uid: String },
108
109 #[error("TLS error: {reason}")]
112 TlsError { reason: String },
113
114 #[error("decompression error: {reason}")]
117 DecompressionError { reason: String },
118
119 #[error("compression error: {reason}")]
121 CompressionError { reason: String },
122
123 #[error("{0}")]
126 Other(String),
127}
128
129pub type DcmResult<T> = Result<T, DcmError>;
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
136pub struct DimseStatus(pub u16);
137
138impl DimseStatus {
139 pub const SUCCESS: Self = Self(0x0000);
140 pub const CANCEL: Self = Self(0xFE00);
141 pub const PENDING: Self = Self(0xFF00);
142 pub const PENDING_WITH_WARNINGS: Self = Self(0xFF01);
143
144 pub const REFUSED_OUT_OF_RESOURCES: Self = Self(0xA700);
146 pub const REFUSED_MOVE_DESTINATION_UNKNOWN: Self = Self(0xA801);
147 pub const ERROR_DATA_SET_DOES_NOT_MATCH: Self = Self(0xA900);
148 pub const ERROR_CANNOT_UNDERSTAND: Self = Self(0xC000);
149
150 pub fn is_success(self) -> bool {
152 self.0 == 0x0000
153 }
154
155 pub fn is_pending(self) -> bool {
157 self.0 == 0xFF00 || self.0 == 0xFF01
158 }
159
160 pub fn is_failure(self) -> bool {
162 matches!(self.0 >> 12, 0xA..=0xC)
164 }
165
166 pub fn is_warning(self) -> bool {
168 self.0 == 0x0001 || (self.0 >> 12 == 0xB && !self.is_failure())
170 }
171}
172
173impl fmt::Display for DimseStatus {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 match *self {
176 Self::SUCCESS => write!(f, "Success (0x0000)"),
177 Self::CANCEL => write!(f, "Cancel (0xFE00)"),
178 Self::PENDING => write!(f, "Pending (0xFF00)"),
179 Self::PENDING_WITH_WARNINGS => write!(f, "Pending with warnings (0xFF01)"),
180 other => write!(f, "Status 0x{:04X}", other.0),
181 }
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn dimse_status_classification() {
191 assert!(DimseStatus::SUCCESS.is_success());
192 assert!(!DimseStatus::SUCCESS.is_failure());
193 assert!(!DimseStatus::SUCCESS.is_pending());
194
195 assert!(DimseStatus::PENDING.is_pending());
196 assert!(DimseStatus::PENDING_WITH_WARNINGS.is_pending());
197
198 assert!(DimseStatus::REFUSED_OUT_OF_RESOURCES.is_failure());
199 assert!(DimseStatus::ERROR_CANNOT_UNDERSTAND.is_failure());
200 }
201
202 #[test]
203 fn error_display() {
204 let err = DcmError::InvalidTag {
205 group: 0x0008,
206 element: 0x0010,
207 reason: "missing".into(),
208 };
209 assert_eq!(err.to_string(), "invalid tag (0008,0010): missing");
210 }
211
212 #[test]
213 fn io_error_conversion() {
214 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
215 let dcm_err: DcmError = io_err.into();
216 assert!(matches!(dcm_err, DcmError::Io(_)));
217 }
218}