Skip to main content

fits_well/
error.rs

1use std::fmt;
2use std::io;
3
4pub type Result<T> = std::result::Result<T, FitsError>;
5
6#[derive(Debug)]
7pub enum FitsError {
8    Io(io::Error),
9    /// A keyword name violated the FITS character set or 8-byte length limit.
10    InvalidKeyword {
11        name: String,
12    },
13    /// A card's value field could not be parsed as any FITS value type.
14    InvalidValue {
15        card: String,
16    },
17    /// `BITPIX` held a value outside {8, 16, 32, 64, −32, −64}.
18    InvalidBitpix {
19        code: i64,
20    },
21    /// A header unit ended (ran out of cards) without an `END` record.
22    MissingEnd,
23    /// A mandatory keyword was absent where the structure requires it.
24    MissingKeyword {
25        name: &'static str,
26    },
27    /// A keyword was present and well-typed but its value lies outside the range
28    /// the standard permits for its role (e.g. `NAXIS > 999`, `PCOUNT < 0`,
29    /// `GCOUNT < 1`, a negative axis length, or a `THEAP` that precedes the heap).
30    KeywordOutOfRange {
31        name: &'static str,
32    },
33    /// The byte stream ended in the middle of a header or data unit.
34    UnexpectedEof,
35    /// The data-unit size implied by the header overflows a 64-bit byte count
36    /// (a malformed or hostile header with absurd `NAXISn`/`PCOUNT`/`GCOUNT`).
37    DataUnitOverflow,
38    /// The data-unit size implied by the header is non-overflowing but too large to
39    /// allocate — a malformed or hostile header declaring an absurd (yet in-range)
40    /// `NAXISn`/`ZNAXISn`/`ZNAXIS2`. The output buffer is allocated fallibly
41    /// (`try_reserve`), so this surfaces as a recoverable error rather than an
42    /// out-of-memory process abort.
43    DataUnitTooLarge {
44        bytes: usize,
45    },
46    /// A decoded data unit held a different element count than the header's
47    /// declared geometry — a corrupt or truncated data unit.
48    DataSizeMismatch {
49        expected: usize,
50        got: usize,
51    },
52    /// A data-unit read named an HDU index beyond the parsed sequence.
53    HduIndexOutOfBounds {
54        index: usize,
55        len: usize,
56    },
57    /// `read_image` was called on an HDU that is not an image array (a table,
58    /// random-groups, or unmodelled extension).
59    NotAnImage,
60    /// An IMAGE/primary HDU carries group structure (`PCOUNT ≠ 0` or `GCOUNT ≠ 1`),
61    /// which a plain image array must not have (§4.3).
62    ImageHasGroups,
63    /// `read_table` was called on an HDU that is not a binary table.
64    NotABinTable,
65    /// `read_groups` was called on an HDU that is not a random-groups primary.
66    NotRandomGroups,
67    /// `read_ascii_table` was called on an HDU that is not an ASCII table.
68    NotAnAsciiTable,
69    /// The decompressor was handed an HDU that is not a tiled-compressed image (no
70    /// `ZIMAGE = T`). `read_image` guards this and returns [`FitsError::NotAnImage`]
71    /// for a plain `BINTABLE`, so this surfaces only via the internal decode path.
72    NotCompressedImage,
73    /// `read_compressed_table` was called on an HDU that is not a tiled-compressed
74    /// table (no `ZTABLE = T`).
75    NotCompressedTable,
76    /// Two mutually-exclusive WCS keyword conventions are both present (e.g. `PC`
77    /// and `CD`, or `CROTA` and `PC`); a conforming header uses only one (§8).
78    ConflictingWcsKeywords {
79        detail: &'static str,
80    },
81    /// A tiled-image compression algorithm or variant is not yet supported.
82    UnsupportedCompression {
83        name: String,
84    },
85    /// A `TFORMn` value could not be parsed as a binary-table column format.
86    InvalidTform {
87        tform: String,
88    },
89    /// `ColumnReader::raw` was called on a variable-length-array (`P`/`Q`) column;
90    /// use `ColumnReader::vla` instead.
91    VariableLengthColumn {
92        code: char,
93    },
94    /// `ColumnReader::vla` was called on a fixed-width column.
95    NotAVla {
96        code: char,
97    },
98    /// `ColumnReader::bits` was called on a column that is not an `X` bit array.
99    NotABitColumn {
100        code: char,
101    },
102    /// `ColumnReader::complex` was called on a column that is not `C`/`M` complex.
103    NotAComplexColumn {
104        code: char,
105    },
106    /// `ColumnReader::physical` was called on a column with no numeric physical
107    /// value (`A`/`L`/`X`/`C`/`M`).
108    NonNumericColumn {
109        code: char,
110    },
111    /// A column index named a field beyond the table's column list.
112    ColumnIndexOutOfBounds {
113        index: usize,
114        len: usize,
115    },
116    /// No column with the requested `TTYPEn` name exists in the table.
117    ColumnNotFound {
118        name: String,
119    },
120    /// The summed column widths disagree with the declared row width (`NAXIS1`).
121    RowWidthMismatch {
122        computed: usize,
123        declared: usize,
124    },
125    /// A requested compression tile shape has a different rank (axis count) than the
126    /// image it tiles. Pass an empty shape for the default row-tiling instead.
127    TileShapeRankMismatch {
128        tile_rank: usize,
129        image_rank: usize,
130    },
131}
132
133impl fmt::Display for FitsError {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        match self {
136            FitsError::Io(e) => write!(f, "I/O error: {e}"),
137            FitsError::InvalidKeyword { name } => write!(f, "invalid keyword name {name:?}"),
138            FitsError::InvalidValue { card } => {
139                write!(f, "cannot parse value field of card {card:?}")
140            }
141            FitsError::InvalidBitpix { code } => write!(f, "invalid BITPIX value {code}"),
142            FitsError::MissingEnd => write!(f, "header unit ended without an END record"),
143            FitsError::MissingKeyword { name } => write!(f, "missing mandatory keyword {name}"),
144            FitsError::KeywordOutOfRange { name } => {
145                write!(f, "keyword {name} has an out-of-range value")
146            }
147            FitsError::UnexpectedEof => write!(f, "unexpected end of stream inside a FITS unit"),
148            FitsError::DataUnitOverflow => {
149                write!(f, "header-implied data-unit size overflows 64 bits")
150            }
151            FitsError::DataUnitTooLarge { bytes } => {
152                write!(
153                    f,
154                    "header-implied data-unit size ({bytes} bytes) is too large to allocate"
155                )
156            }
157            FitsError::DataSizeMismatch { expected, got } => {
158                write!(
159                    f,
160                    "decoded data unit has {got} elements, header implies {expected}"
161                )
162            }
163            FitsError::HduIndexOutOfBounds { index, len } => {
164                write!(f, "HDU index {index} out of bounds (file has {len} HDUs)")
165            }
166            FitsError::NotAnImage => write!(f, "HDU is not an image array"),
167            FitsError::ImageHasGroups => {
168                write!(
169                    f,
170                    "image HDU has group structure (PCOUNT ≠ 0 or GCOUNT ≠ 1)"
171                )
172            }
173            FitsError::NotABinTable => write!(f, "HDU is not a binary table"),
174            FitsError::NotRandomGroups => write!(f, "HDU is not a random-groups primary"),
175            FitsError::NotAnAsciiTable => write!(f, "HDU is not an ASCII table"),
176            FitsError::NotCompressedImage => write!(f, "HDU is not a tiled-compressed image"),
177            FitsError::NotCompressedTable => write!(f, "HDU is not a tiled-compressed table"),
178            FitsError::ConflictingWcsKeywords { detail } => {
179                write!(f, "conflicting WCS keywords: {detail}")
180            }
181            FitsError::UnsupportedCompression { name } => {
182                write!(f, "unsupported tiled compression: {name}")
183            }
184            FitsError::InvalidTform { tform } => write!(f, "invalid column format {tform:?}"),
185            FitsError::VariableLengthColumn { code } => write!(
186                f,
187                "column format '{code}' is a variable-length array; use the column reader's vla()"
188            ),
189            FitsError::NotAVla { code } => {
190                write!(f, "column format '{code}' is not a variable-length array")
191            }
192            FitsError::NotABitColumn { code } => {
193                write!(f, "column format '{code}' is not an X bit array")
194            }
195            FitsError::NotAComplexColumn { code } => {
196                write!(f, "column format '{code}' is not a C/M complex column")
197            }
198            FitsError::NonNumericColumn { code } => {
199                write!(f, "column format '{code}' has no numeric physical value")
200            }
201            FitsError::ColumnIndexOutOfBounds { index, len } => {
202                write!(
203                    f,
204                    "column index {index} out of bounds (table has {len} columns)"
205                )
206            }
207            FitsError::ColumnNotFound { name } => {
208                write!(f, "no column named {name:?} in the table")
209            }
210            FitsError::RowWidthMismatch { computed, declared } => write!(
211                f,
212                "column widths sum to {computed} bytes but NAXIS1 declares {declared}"
213            ),
214            FitsError::TileShapeRankMismatch {
215                tile_rank,
216                image_rank,
217            } => write!(
218                f,
219                "tile shape has {tile_rank} axes but the image has {image_rank}"
220            ),
221        }
222    }
223}
224
225impl std::error::Error for FitsError {
226    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
227        match self {
228            FitsError::Io(e) => Some(e),
229            _ => None,
230        }
231    }
232}
233
234impl From<io::Error> for FitsError {
235    fn from(e: io::Error) -> Self {
236        FitsError::Io(e)
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn display_messages_are_specific() {
246        assert_eq!(
247            FitsError::InvalidBitpix { code: 7 }.to_string(),
248            "invalid BITPIX value 7"
249        );
250        assert_eq!(
251            FitsError::DataUnitOverflow.to_string(),
252            "header-implied data-unit size overflows 64 bits"
253        );
254        assert_eq!(
255            FitsError::DataUnitTooLarge { bytes: 1 << 60 }.to_string(),
256            "header-implied data-unit size (1152921504606846976 bytes) is too large to allocate"
257        );
258        assert_eq!(
259            FitsError::MissingKeyword { name: "NAXIS" }.to_string(),
260            "missing mandatory keyword NAXIS"
261        );
262    }
263
264    #[test]
265    fn io_error_is_preserved_as_source() {
266        let io_err = io::Error::new(io::ErrorKind::UnexpectedEof, "boom");
267        let err = FitsError::from(io_err);
268        assert!(matches!(err, FitsError::Io(_)));
269        assert!(std::error::Error::source(&err).is_some());
270    }
271}