Skip to main content

dicom_toolkit_core/
error.rs

1//! DICOM error types, replacing DCMTK's `OFCondition` system.
2//!
3//! Every fallible operation returns `DcmResult<T>` which is an alias for
4//! `Result<T, DcmError>`.
5
6use std::fmt;
7
8/// Central error type for all dcmtk-rs operations.
9///
10/// Mirrors the module+code+text structure of DCMTK's `OFCondition` but uses
11/// Rust's enum-based error handling for exhaustive matching.
12#[derive(Debug, thiserror::Error)]
13pub enum DcmError {
14    // ── I/O ──────────────────────────────────────────────────────────────
15    /// Wraps a `std::io::Error`.
16    #[error("I/O error: {0}")]
17    Io(#[from] std::io::Error),
18
19    /// Unexpected end of data while parsing.
20    #[error("unexpected end of data at offset {offset}")]
21    UnexpectedEof { offset: u64 },
22
23    // ── Parsing / Encoding ───────────────────────────────────────────────
24    /// The DICOM preamble or prefix ("DICM") is invalid.
25    #[error("invalid DICOM file: {reason}")]
26    InvalidFile { reason: String },
27
28    /// A tag could not be interpreted.
29    #[error("invalid tag ({group:04X},{element:04X}): {reason}")]
30    InvalidTag {
31        group: u16,
32        element: u16,
33        reason: String,
34    },
35
36    /// Value representation mismatch.
37    #[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    /// A value could not be decoded from the underlying bytes.
46    #[error("invalid value for tag ({group:04X},{element:04X}): {reason}")]
47    InvalidValue {
48        group: u16,
49        element: u16,
50        reason: String,
51    },
52
53    /// An element has an invalid or unsupported length.
54    #[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    // ── Transfer Syntax ──────────────────────────────────────────────────
62    /// The transfer syntax UID is not recognized or not supported.
63    #[error("unsupported transfer syntax: {uid}")]
64    UnsupportedTransferSyntax { uid: String },
65
66    /// No codec is registered for a compressed transfer syntax.
67    #[error("no codec available for transfer syntax: {uid}")]
68    NoCodec { uid: String },
69
70    // ── Data Dictionary ──────────────────────────────────────────────────
71    /// A tag was not found in the data dictionary.
72    #[error("unknown tag ({group:04X},{element:04X})")]
73    UnknownTag { group: u16, element: u16 },
74
75    // ── UID ──────────────────────────────────────────────────────────────
76    /// A UID string is syntactically invalid.
77    #[error("invalid UID: {reason}")]
78    InvalidUid { reason: String },
79
80    // ── Character Encoding ───────────────────────────────────────────────
81    /// Character set conversion failed.
82    #[error("character encoding error: {reason}")]
83    CharsetError { reason: String },
84
85    // ── Network ──────────────────────────────────────────────────────────
86    /// DICOM association was rejected by the remote peer.
87    #[error("association rejected: {reason}")]
88    AssociationRejected { reason: String },
89
90    /// DICOM association was aborted.
91    #[error("association aborted: abort_source={abort_source}, reason={reason}")]
92    AssociationAborted {
93        abort_source: String,
94        reason: String,
95    },
96
97    /// A DIMSE operation failed with a status code.
98    #[error("DIMSE error: status 0x{status:04X} ({description})")]
99    DimseError { status: u16, description: String },
100
101    /// Network timeout.
102    #[error("network timeout after {seconds}s")]
103    Timeout { seconds: u64 },
104
105    /// Presentation context negotiation failed.
106    #[error("no accepted presentation context for SOP class {sop_class_uid}")]
107    NoPresentationContext { sop_class_uid: String },
108
109    // ── TLS ──────────────────────────────────────────────────────────────
110    /// TLS handshake or transport error.
111    #[error("TLS error: {reason}")]
112    TlsError { reason: String },
113
114    // ── Codec ────────────────────────────────────────────────────────────
115    /// Image decompression failed.
116    #[error("decompression error: {reason}")]
117    DecompressionError { reason: String },
118
119    /// Image compression failed.
120    #[error("compression error: {reason}")]
121    CompressionError { reason: String },
122
123    // ── Generic ──────────────────────────────────────────────────────────
124    /// Catch-all for errors that don't fit other variants.
125    #[error("{0}")]
126    Other(String),
127}
128
129/// Convenience alias used throughout the crate.
130pub type DcmResult<T> = Result<T, DcmError>;
131
132/// DICOM status codes returned in DIMSE responses.
133///
134/// Mirrors the status code definitions from DCMTK's `dimse.h`.
135#[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    // Failure codes
145    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    /// Returns `true` if this status indicates success.
151    pub fn is_success(self) -> bool {
152        self.0 == 0x0000
153    }
154
155    /// Returns `true` if this status indicates a pending response (more results follow).
156    pub fn is_pending(self) -> bool {
157        self.0 == 0xFF00 || self.0 == 0xFF01
158    }
159
160    /// Returns `true` if this status indicates a failure.
161    pub fn is_failure(self) -> bool {
162        // Failures occupy ranges 0xAxxx, 0xBxxx, 0xCxxx
163        matches!(self.0 >> 12, 0xA..=0xC)
164    }
165
166    /// Returns `true` if this status indicates a warning.
167    pub fn is_warning(self) -> bool {
168        // Warnings: 0x0001 and 0xB000-range
169        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}