fits_well/hdu/mod.rs
1//! HDU classification and the data-unit sizing formula.
2//!
3//! Boundaries are computable from the header alone — `Nbits = |BITPIX| · GCOUNT ·
4//! (PCOUNT + Π NAXISn)`, rounded up to a block — so the reader never touches data
5//! to find the next HDU.
6
7use crate::block::padded_len;
8use crate::error::FitsError;
9use crate::error::Result;
10use crate::header::Header;
11
12/// The structural kind of an HDU, inferred from its mandatory keywords.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum HduKind {
15 /// Primary array (`SIMPLE = T`), possibly empty (`NAXIS = 0`).
16 Primary,
17 /// `XTENSION = 'IMAGE'` — same data model as the primary array.
18 Image,
19 /// `XTENSION = 'TABLE'` — ASCII table.
20 AsciiTable,
21 /// `XTENSION = 'BINTABLE'` — binary table (with optional heap).
22 BinTable,
23 /// A tiled-compressed image (§10.1): structurally a `BINTABLE` with `ZIMAGE = T`.
24 /// `read_image` reads it like any other image.
25 CompressedImage,
26 /// A tiled-compressed table (§10.3): structurally a `BINTABLE` with `ZTABLE = T`.
27 /// `read_compressed_table` uncompresses it.
28 CompressedTable,
29 /// Legacy random-groups primary (`GROUPS = T`, `NAXIS1 = 0`). Read-only.
30 RandomGroups,
31 /// A conforming extension whose `XTENSION` value this crate does not model.
32 Other,
33}
34
35impl HduKind {
36 pub(crate) fn classify(header: &Header) -> HduKind {
37 // `Value::Text` already stripped the trailing spaces of `'IMAGE '` etc.
38 if let Some(xtension) = header.get_text("XTENSION") {
39 match xtension {
40 "IMAGE" => HduKind::Image,
41 "TABLE" => HduKind::AsciiTable,
42 // §10: a tiled-compressed image/table rides inside a BINTABLE,
43 // flagged by ZIMAGE/ZTABLE — classify by the payload, not the
44 // container, so callers see what they can actually read.
45 "BINTABLE" if header.get_logical("ZIMAGE") == Some(true) => {
46 HduKind::CompressedImage
47 }
48 "BINTABLE" if header.get_logical("ZTABLE") == Some(true) => {
49 HduKind::CompressedTable
50 }
51 "BINTABLE" => HduKind::BinTable,
52 _ => HduKind::Other,
53 }
54 } else if header.get_logical("GROUPS") == Some(true) {
55 HduKind::RandomGroups
56 } else {
57 HduKind::Primary
58 }
59 }
60}
61
62/// The size of an HDU's data unit, both as the raw bit-count-derived byte length
63/// and rounded to the on-disk block grid.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub(crate) struct DataExtent {
66 /// Unpadded data length (`Nbits / 8`).
67 pub(crate) data_bytes: u64,
68 /// Length rounded up to the 2880-byte grid — the bytes occupied on disk.
69 pub(crate) padded_bytes: u64,
70}
71
72/// Compute the data-unit extent from a parsed header (Eq. 2).
73pub(crate) fn data_extent(header: &Header) -> Result<DataExtent> {
74 let elem = header.bitpix()?.elem_size() as u64;
75 let axes = header.axes()?;
76 // PCOUNT/GCOUNT are mandatory ≥0 / ≥1 integers; a present-but-out-of-range
77 // value is malformed and must not be silently clamped (it would yield a
78 // plausible-but-wrong extent and a bad seek). Absence keeps the primary/IMAGE
79 // defaults of 0 and 1.
80 let pcount = match header.get_integer("PCOUNT") {
81 Some(p) if p < 0 => return Err(FitsError::KeywordOutOfRange { name: "PCOUNT" }),
82 Some(p) => p as u64,
83 None => 0,
84 };
85 let gcount = match header.get_integer("GCOUNT") {
86 Some(g) if g < 1 => return Err(FitsError::KeywordOutOfRange { name: "GCOUNT" }),
87 Some(g) => g as u64,
88 None => 1,
89 };
90 let random_groups = header.get_logical("GROUPS") == Some(true);
91
92 // `NAXIS = 0` means no data array at all (the empty product would be 1, not
93 // 0). Otherwise multiply the axis lengths — skipping the leading zero sentinel
94 // for random groups. All arithmetic is checked: the axis lengths come from an
95 // untrusted file and an overflowed product would drive a wild allocation in
96 // the reader.
97 let array_elems = if axes.is_empty() {
98 0
99 } else {
100 let array_axes: &[usize] = if random_groups { &axes[1..] } else { &axes };
101 array_axes
102 .iter()
103 .try_fold(1u64, |acc, &n| acc.checked_mul(n as u64))
104 .ok_or(FitsError::DataUnitOverflow)?
105 };
106
107 let group_size = pcount
108 .checked_add(array_elems)
109 .ok_or(FitsError::DataUnitOverflow)?;
110 let data_bytes = elem
111 .checked_mul(gcount)
112 .and_then(|n| n.checked_mul(group_size))
113 .ok_or(FitsError::DataUnitOverflow)?;
114 Ok(DataExtent {
115 data_bytes,
116 padded_bytes: padded_len(data_bytes),
117 })
118}
119
120#[cfg(test)]
121mod tests;