fits-well 0.1.2

A blazing-fast reader and writer for FITS (Flexible Image Transport System) files, targeting the full FITS 4.0 standard.
Documentation
//! The 2880-byte block grid — the I/O quantum of FITS.
//!
//! Every header unit and every data unit occupies a whole number of 2880-byte
//! blocks, with the final block padded to fill the boundary. Because of this, a
//! conforming file's length on disk is always a multiple of [`BLOCK_SIZE`].

/// The fundamental layout unit: 2880 bytes = 36 × 80-byte cards.
pub const BLOCK_SIZE: usize = 2880;

/// A keyword record (card) is 80 bytes of restricted ASCII.
pub const CARD_SIZE: usize = 80;

/// Fill byte for header units and ASCII-table data units: ASCII space.
pub(crate) const SPACE_FILL: u8 = b' ';

/// Fill byte for all data units except ASCII tables: NUL (all bits zero).
pub(crate) const ZERO_FILL: u8 = 0;

/// Number of whole 2880-byte blocks needed to hold `len` bytes, rounding up.
///
/// `blocks_for(0) == 0` — a zero-length unit (e.g. `NAXIS = 0` data) occupies no
/// blocks at all.
fn blocks_for(len: u64) -> u64 {
    len.div_ceil(BLOCK_SIZE as u64)
}

/// `len` rounded up to the next 2880-byte boundary (the on-disk unit length).
///
/// Saturating: an absurd `len` (within `2880` of `u64::MAX`, only reachable from
/// a hostile header) clamps to `u64::MAX` rather than wrapping to a too-small
/// length that would corrupt the next-HDU seek. `data_extent` already rejects
/// such sizes upstream; this keeps the rounding itself defense-complete.
pub(crate) fn padded_len(len: u64) -> u64 {
    blocks_for(len).saturating_mul(BLOCK_SIZE as u64)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn block_geometry_constants_are_consistent() {
        assert_eq!(BLOCK_SIZE, 2880);
        assert_eq!(CARD_SIZE, 80);
        // Exactly 36 cards of 80 bytes fill a 2880-byte block.
        assert_eq!(BLOCK_SIZE / CARD_SIZE, 36);
        assert_eq!(BLOCK_SIZE % CARD_SIZE, 0);
    }

    #[test]
    fn blocks_for_rounds_up_at_the_boundary() {
        // (input bytes, expected blocks)
        let cases = [
            (0u64, 0u64),
            (1, 1),
            (2879, 1),
            (2880, 1),
            (2881, 2),
            (5760, 2),
            (5761, 3),
        ];
        for (len, blocks) in cases {
            assert_eq!(blocks_for(len), blocks, "blocks_for({len})");
            assert_eq!(
                padded_len(len),
                blocks * BLOCK_SIZE as u64,
                "padded_len({len})"
            );
        }
    }

    #[test]
    fn padded_len_is_idempotent_on_aligned_input() {
        for blocks in [0u64, 1, 2, 199] {
            let aligned = blocks * BLOCK_SIZE as u64;
            assert_eq!(padded_len(aligned), aligned);
        }
    }

    #[test]
    fn padded_len_saturates_instead_of_wrapping() {
        // An absurd length (only reachable from a hostile header) must clamp to
        // u64::MAX, never wrap: `blocks_for(u64::MAX) · 2880` overflows u64, and a
        // wrapping multiply yields a value far *smaller* than the input — which
        // would corrupt the next-HDU seek. Saturating keeps padded_len ≥ its input.
        assert_eq!(padded_len(u64::MAX), u64::MAX);
        // Demonstrate the naive multiply really would wrap to something tiny.
        let wrapped = blocks_for(u64::MAX).wrapping_mul(BLOCK_SIZE as u64);
        assert!(
            wrapped < u64::MAX,
            "the unguarded multiply wraps below the input"
        );
    }
}