#![deny(missing_docs)]
use rubbl_core::io::EofReadExactExt;
use std::io::prelude::*;
use std::io::SeekFrom;
use std::str;
use thiserror::Error;
#[allow(missing_docs)]
#[derive(Debug, Error)]
pub enum FitsError {
#[error("FITS stream should be a multiple of 2880 bytes long; got {0}")]
Not2880Multiple(u64),
#[error("file does not appear to be in FITS format")]
InvalidFormat,
#[error("second FITS header must be BITPIX")]
SecondHeaderNotBitpix,
#[error("unsupported BITPIX value in FITS file: {0}")]
UnsupportedBitpix(isize),
#[error("third FITS header must be NAXIS")]
ThirdHeaderNotNaxis,
#[error("unsupported NAXIS value in FITS file: {0}")]
BadNaxisValue(isize),
#[error("illegal negative FITS GCOUNT value")]
NegativeGcountValue,
#[error("illegal extension HDU without EXTNAME header")]
ExtHduWithoutExtname,
#[error("illegal negative FITS group size")]
NegativeGroupSize,
#[error("truncated-looking FITS file")]
Truncated,
#[error("FITS data stream does not begin with \"SIMPLE = T\" marker")]
NotSimple,
#[error("illegal header keyword ASCII code {0}")]
BadHeaderAscii(u8),
#[error("malformed FITS header keyword")]
MalformedHeader,
#[error("{0}")]
FitsFormat(#[from] FitsFormatError),
#[error("{0}")]
IO(#[from] std::io::Error),
}
#[allow(missing_docs)]
#[derive(Debug, Error)]
pub enum FitsFormatError {
#[error("expected opening equals and quote in fixed-format string record")]
UnexpectedStrSep,
#[error("illegal non-printable-ASCII value in fixed-format string record")]
IllegalAscii,
#[error("illegal ASCII value {0} after string in fixed-format string record")]
IllegalAsciiAfterString(u8),
#[error("illegal ASCII value {0} after single quote in fixed-format string record")]
IllegalAsciiAfterQuote(u8),
#[error("illegal unterminated fixed-format string record")]
Unterminated,
#[error("expected space or slash in byte 30 of fixed-format integer record")]
UnexpectedByte30,
#[error("empty record that should have been a fixed-format integer")]
EmptyRecordInt,
#[error("expected digit but got ASCII {0:?} in fixed-format integer")]
ExpectedDigit(u8),
#[error("malformed FITS NAXIS header")]
MalformedNaxis,
#[error("expected digit but got ASCII {0:?} in NAXIS header")]
ExpectedDigitNaxisHeader(u8),
#[error("expected space but got ASCII {0:?} in NAXIS header")]
ExpectedSpaceNaxisHeader(u8),
#[error("misnumbered NAXIS header (expected {expected}, got {got})")]
MisnumberedNaxis { expected: usize, got: usize },
#[error("illegal negative NAXIS{value} value {n}")]
NegativeNaxisValue { value: usize, n: isize },
#[error("{0}")]
Utf8(#[from] std::str::Utf8Error),
}
#[derive(Clone, Debug)]
pub enum LowLevelFitsItem<'a> {
Header(&'a [u8]),
EndOfHeaders(usize),
Data(&'a [u8]),
SpecialRecordData(&'a [u8]),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(i8)]
pub enum Bitpix {
U8 = 8,
I16 = 16,
I32 = 32,
I64 = 64,
F32 = -32,
F64 = -64,
}
impl Bitpix {
pub fn n_bytes(&self) -> usize {
match *self {
Bitpix::U8 => 1,
Bitpix::I16 => 2,
Bitpix::I32 => 4,
Bitpix::I64 => 8,
Bitpix::F32 => 4,
Bitpix::F64 => 8,
}
}
}
#[derive(Clone)] pub struct FitsDecoder<R: Read> {
inner: R,
buf: [u8; 2880],
offset: usize,
state: DecoderState,
hdu_num: usize,
bitpix: Bitpix,
naxis: Vec<usize>,
primary_seen_groups: bool,
pcount: isize,
gcount: usize,
data_remaining: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum DecoderState {
Beginning,
SizingHeaders,
OtherHeaders,
Data,
NewHdu,
SpecialRecords,
}
const FITS_MARKER: &[u8] = b"SIMPLE = T";
const XTENSION_MARKER: &[u8] = b"XTENSION= ";
const BITPIX_MARKER: &[u8] = b"BITPIX = ";
const NAXIS_MARKER: &[u8] = b"NAXIS = ";
const END_MARKER: &[u8] =
b"END ";
const GROUPS_MARKER: &[u8] = b"GROUPS = T";
const PCOUNT_MARKER: &[u8] = b"PCOUNT = ";
const GCOUNT_MARKER: &[u8] = b"GCOUNT = ";
const EXTNAME_MARKER: &[u8] = b"EXTNAME = ";
impl<R: Read> FitsDecoder<R> {
pub fn new(inner: R) -> Self {
Self {
inner,
buf: [0; 2880],
offset: 2880,
state: DecoderState::Beginning,
hdu_num: 0,
bitpix: Bitpix::U8,
naxis: Vec::new(),
primary_seen_groups: false,
pcount: 0,
gcount: 1,
data_remaining: 0,
}
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Result<Option<LowLevelFitsItem<'_>>, FitsError> {
if self.offset == 2880 {
if !self.inner.eof_read_exact::<FitsError>(&mut self.buf)? {
if self.state != DecoderState::NewHdu && self.state != DecoderState::SpecialRecords
{
return Err(FitsError::Truncated);
}
return Ok(None);
}
self.offset = 0;
}
if self.state == DecoderState::Data {
if self.data_remaining > 2880 {
self.offset = 2880;
self.data_remaining -= 2880;
return Ok(Some(LowLevelFitsItem::Data(&self.buf)));
}
let slice = &self.buf[..self.data_remaining];
self.state = DecoderState::NewHdu;
self.offset = 2880;
self.bitpix = Bitpix::U8;
self.gcount = 1;
self.pcount = 0;
self.naxis.clear();
self.data_remaining = 0;
self.primary_seen_groups = true; return Ok(Some(LowLevelFitsItem::Data(slice)));
}
if self.state == DecoderState::SpecialRecords {
self.offset = 2880;
return Ok(Some(LowLevelFitsItem::SpecialRecordData(&self.buf)));
}
let record = &self.buf[self.offset..self.offset + 80];
self.offset += 80;
if self.state == DecoderState::Beginning {
if &record[..FITS_MARKER.len()] != FITS_MARKER {
return Err(FitsError::NotSimple);
}
self.state = DecoderState::SizingHeaders;
return Ok(Some(LowLevelFitsItem::Header(record)));
}
if self.state == DecoderState::NewHdu {
if &record[..XTENSION_MARKER.len()] != XTENSION_MARKER {
self.state = DecoderState::SpecialRecords;
return Ok(Some(LowLevelFitsItem::SpecialRecordData(&self.buf)));
}
self.state = DecoderState::SizingHeaders;
self.hdu_num += 1;
return Ok(Some(LowLevelFitsItem::Header(record)));
}
if self.state == DecoderState::SizingHeaders {
let mut keep_going = false;
if &record[..BITPIX_MARKER.len()] == BITPIX_MARKER {
let bitpix = parse_fixed_int(record)?;
self.bitpix = match bitpix {
8 => Bitpix::U8,
16 => Bitpix::I16,
32 => Bitpix::I32,
64 => Bitpix::I64,
-32 => Bitpix::F32,
-64 => Bitpix::F64,
other => {
return Err(FitsError::UnsupportedBitpix(other));
}
};
} else if &record[..NAXIS_MARKER.len()] == NAXIS_MARKER {
let naxis = parse_fixed_int(record)?;
if !(0..=999).contains(&naxis) {
return Err(FitsError::BadNaxisValue(naxis));
}
self.naxis.clear();
self.naxis.reserve(naxis as usize);
} else if accumulate_naxis_value(record, &mut self.naxis)? {
} else {
keep_going = true;
self.state = DecoderState::OtherHeaders;
}
if !keep_going {
return Ok(Some(LowLevelFitsItem::Header(record)));
}
}
if self.state == DecoderState::OtherHeaders {
if &record[..GROUPS_MARKER.len()] == GROUPS_MARKER {
self.primary_seen_groups = true;
} else if self.primary_seen_groups && &record[..PCOUNT_MARKER.len()] == PCOUNT_MARKER {
self.pcount = parse_fixed_int(record)?;
} else if self.primary_seen_groups && &record[..GCOUNT_MARKER.len()] == GCOUNT_MARKER {
let n = parse_fixed_int(record)?;
if n < 0 {
return Err(FitsError::NegativeGcountValue);
}
self.gcount = n as usize;
} else if record == END_MARKER {
let group_size = if self.hdu_num == 0 && self.primary_seen_groups {
self.pcount + self.naxis.iter().skip(1).product::<usize>() as isize
} else {
self.pcount + self.naxis.iter().product::<usize>() as isize
};
if group_size < 0 {
return Err(FitsError::NegativeGroupSize);
}
self.offset = 2880;
self.data_remaining = self.bitpix.n_bytes() * self.gcount * group_size as usize;
if self.data_remaining != 0 {
self.state = DecoderState::Data;
} else {
self.state = DecoderState::NewHdu;
self.offset = 2880;
self.bitpix = Bitpix::U8;
self.gcount = 1;
self.pcount = 0;
self.naxis.clear();
self.primary_seen_groups = true; }
return Ok(Some(LowLevelFitsItem::EndOfHeaders(self.data_remaining)));
}
let mut i = 0;
while i < 8 {
match record[i] {
0x30..=0x39 => {} 0x41..=0x5A => {} b'_' => {}
b'-' => {}
b' ' => {
break;
}
other => {
return Err(FitsError::BadHeaderAscii(other));
}
}
i += 1;
}
while i < 8 {
if record[i] != b' ' {
return Err(FitsError::MalformedHeader);
}
i += 1;
}
return Ok(Some(LowLevelFitsItem::Header(record)));
}
Ok(None)
}
pub fn into_inner(self) -> R {
self.inner
}
}
#[derive(Clone, Debug)]
pub struct FitsParser<R: Read + Seek> {
inner: R,
hdus: Vec<ParsedHdu>,
#[allow(dead_code)]
special_record_size: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum HduKind {
PrimaryArray,
PrimaryRandomGroups,
PrimaryNoData,
ImageExtension,
AsciiTableExtension,
BinaryTableExtension,
OtherExtension(String),
}
#[derive(Clone, Debug)]
pub struct ParsedHdu {
kind: HduKind,
name: String,
#[allow(dead_code)]
header_offset: u64,
#[allow(dead_code)]
n_header_records: usize,
bitpix: Bitpix,
pcount: isize,
gcount: usize,
naxis: Vec<usize>,
}
impl<R: Read + Seek> FitsParser<R> {
pub fn new(mut inner: R) -> Result<Self, FitsError> {
let file_size = inner.seek(SeekFrom::End(0))?;
if file_size % 2880 != 0 {
return Err(FitsError::Not2880Multiple(file_size));
}
inner.seek(SeekFrom::Start(0))?;
let mut hdus = Vec::new();
let mut buf = [0u8; 2880];
let mut cur_offset = 0; let mut hdu_header_offset = 0; let mut special_record_size = 0;
loop {
inner.read_exact(&mut buf)?;
cur_offset += 2880;
let mut kind = HduKind::PrimaryArray;
if hdus.is_empty() {
if &buf[..FITS_MARKER.len()] != FITS_MARKER {
return Err(FitsError::InvalidFormat);
}
} else {
if &buf[..XTENSION_MARKER.len()] != XTENSION_MARKER {
special_record_size = file_size - hdu_header_offset;
break;
}
kind = match parse_fixed_string(&buf[..80])?.as_ref() {
"IMAGE" => HduKind::ImageExtension,
"TABLE" => HduKind::AsciiTableExtension,
"BINTABLE" => HduKind::BinaryTableExtension,
other => HduKind::OtherExtension(other.to_owned()), };
}
let bitpix_value = {
let record = &buf[80..160];
if &record[..BITPIX_MARKER.len()] != BITPIX_MARKER {
return Err(FitsError::SecondHeaderNotBitpix);
}
parse_fixed_int(record)?
};
let bitpix = match bitpix_value {
8 => Bitpix::U8,
16 => Bitpix::I16,
32 => Bitpix::I32,
64 => Bitpix::I64,
-32 => Bitpix::F32,
-64 => Bitpix::F64,
other => {
return Err(FitsError::UnsupportedBitpix(other));
}
};
let mut naxis = Vec::new();
let naxis_value = {
let record = &buf[160..240];
if &record[..NAXIS_MARKER.len()] != NAXIS_MARKER {
return Err(FitsError::ThirdHeaderNotNaxis);
}
parse_fixed_int(record)?
};
if !(0..=999).contains(&naxis_value) {
return Err(FitsError::BadNaxisValue(naxis_value));
}
naxis.reserve(naxis_value as usize);
let mut buf_offset = 240;
let mut seen_groups = !hdus.is_empty(); let mut pcount = 0;
let mut gcount = 1;
let mut n_header_records = 3; let mut extname = None;
loop {
if buf_offset == 2880 {
inner.read_exact(&mut buf)?;
cur_offset += 2880;
buf_offset = 0;
}
let record = &buf[buf_offset..buf_offset + 80];
if accumulate_naxis_value(record, &mut naxis)? {
} else if &record[..GROUPS_MARKER.len()] == GROUPS_MARKER {
seen_groups = true;
} else if seen_groups && &record[..PCOUNT_MARKER.len()] == PCOUNT_MARKER {
pcount = parse_fixed_int(record)?;
} else if seen_groups && &record[..GCOUNT_MARKER.len()] == GCOUNT_MARKER {
let n = parse_fixed_int(record)?;
if n < 0 {
return Err(FitsError::NegativeGcountValue);
}
gcount = n as usize;
} else if &record[..EXTNAME_MARKER.len()] == EXTNAME_MARKER {
extname = Some(parse_fixed_string(record)?);
} else if record == END_MARKER {
break;
}
n_header_records += 1;
buf_offset += 80;
}
let extname = if hdus.is_empty() {
"".to_owned()
} else {
match extname {
Some(s) => s,
None => {
return Err(FitsError::ExtHduWithoutExtname);
}
}
};
if seen_groups && hdus.is_empty() {
naxis.remove(0); }
let group_size = pcount + naxis.iter().product::<usize>() as isize;
if group_size < 0 {
return Err(FitsError::NegativeGroupSize);
}
let data_size = bitpix.n_bytes() * gcount * group_size as usize;
if hdus.is_empty() {
kind = if data_size == 0 {
HduKind::PrimaryNoData
} else if seen_groups {
HduKind::PrimaryRandomGroups
} else {
HduKind::PrimaryArray
};
}
hdus.push(ParsedHdu {
kind,
name: extname,
header_offset: hdu_header_offset,
n_header_records,
bitpix,
pcount,
gcount,
naxis,
});
hdu_header_offset = cur_offset + (data_size.div_ceil(2880) * 2880) as u64;
if hdu_header_offset == file_size {
break;
}
inner.seek(SeekFrom::Start(hdu_header_offset))?;
}
Ok(Self {
inner,
hdus,
special_record_size,
})
}
pub fn hdus(&self) -> &[ParsedHdu] {
&self.hdus[..]
}
pub fn into_inner(self) -> R {
self.inner
}
}
impl ParsedHdu {
pub fn extname(&self) -> &str {
&self.name
}
pub fn kind(&self) -> HduKind {
self.kind.clone()
}
pub fn bitpix(&self) -> Bitpix {
self.bitpix
}
pub fn shape(&self) -> (usize, isize, &[usize]) {
(self.gcount, self.pcount, &self.naxis[..])
}
}
fn parse_fixed_int(record: &[u8]) -> Result<isize, FitsFormatError> {
if record[30] != b' ' && record[30] != b'/' {
return Err(FitsFormatError::UnexpectedByte30);
}
let mut i = 10;
while i < 30 {
if record[i] != b' ' {
break;
}
i += 1;
}
if i == 30 {
return Err(FitsFormatError::EmptyRecordInt);
}
let mut negate = false;
if record[i] == b'-' {
negate = true;
i += 1;
} else if record[i] == b'+' {
i += 1;
}
if i == 30 {
return Err(FitsFormatError::EmptyRecordInt);
}
let mut value = 0;
while i < 30 {
value *= 10;
match record[i] {
b'0' => {}
b'1' => {
value += 1;
}
b'2' => {
value += 2;
}
b'3' => {
value += 3;
}
b'4' => {
value += 4;
}
b'5' => {
value += 5;
}
b'6' => {
value += 6;
}
b'7' => {
value += 7;
}
b'8' => {
value += 8;
}
b'9' => {
value += 9;
}
other => {
return Err(FitsFormatError::ExpectedDigit(other));
}
}
i += 1;
}
if negate {
value *= -1;
}
Ok(value)
}
#[cfg(test)]
#[test]
fn fixed_int_parsing() {
let r = b"NAXIS = 999 / comment ";
assert_eq!(parse_fixed_int(r).unwrap(), 999);
let r = b"NAXIS = 2147483647 / comment ";
assert_eq!(parse_fixed_int(r).unwrap(), 2147483647);
let r = b"NAXIS = -2147483648 / comment ";
assert_eq!(parse_fixed_int(r).unwrap(), -2147483648);
let r = b"NAXIS = 999/ comment ";
assert_eq!(parse_fixed_int(r).unwrap(), 999);
let r = b"NAXIS = 999 / comment ";
assert_eq!(parse_fixed_int(r).unwrap(), 999);
let r = b"NAXIS = +999 / comment ";
assert_eq!(parse_fixed_int(r).unwrap(), 999);
let r = b"NAXIS = -999 / comment ";
assert_eq!(parse_fixed_int(r).unwrap(), -999);
let r = b"NAXIS = -0000000000000000999 / comment ";
assert_eq!(parse_fixed_int(r).unwrap(), -999);
let r = b"NAXIS = A 9 / comment ";
assert!(parse_fixed_int(r).is_err());
let r = b"NAXIS = 9A / comment ";
assert!(parse_fixed_int(r).is_err());
}
fn parse_fixed_string(record: &[u8]) -> Result<String, FitsFormatError> {
if &record[8..11] != b"= '" {
return Err(FitsFormatError::UnexpectedStrSep);
}
let mut buf = [0u8; 69];
let mut n_chars = 0;
let mut any_chars = false;
let mut last_non_blank_pos = 0;
let mut state = State::Chars;
const SINGLE_QUOTE: u8 = 0x27;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum State {
Chars,
JustSawSingleQuote,
PreCommentSpaces,
Comment,
}
for i in 0..69 {
let c = record[i + 11];
if !(0x20..=0x7E).contains(&c) {
return Err(FitsFormatError::IllegalAscii);
}
match state {
State::Chars => {
if c == SINGLE_QUOTE {
state = State::JustSawSingleQuote;
} else {
buf[n_chars] = c;
if c != b' ' {
last_non_blank_pos = n_chars;
}
n_chars += 1;
any_chars = true;
}
}
State::JustSawSingleQuote => match c {
SINGLE_QUOTE => {
buf[n_chars] = SINGLE_QUOTE;
last_non_blank_pos = n_chars;
n_chars += 1;
any_chars = true;
state = State::Chars;
}
b' ' => {
state = State::PreCommentSpaces;
}
b'/' => {
state = State::Comment;
}
other => {
return Err(FitsFormatError::IllegalAsciiAfterQuote(other));
}
},
State::PreCommentSpaces => match c {
b' ' => {}
b'/' => {
state = State::Comment;
}
other => {
return Err(FitsFormatError::IllegalAsciiAfterString(other));
}
},
State::Comment => {
break;
}
}
}
if state == State::Chars {
return Err(FitsFormatError::Unterminated);
}
Ok(if !any_chars {
""
} else {
str::from_utf8(&buf[..last_non_blank_pos + 1])?
}
.to_owned())
}
#[cfg(test)]
#[test]
fn fixed_string_parsing() {
let r = b"XTENSION= 'hello' ";
assert_eq!(parse_fixed_string(r).unwrap(), "hello");
let r = b"XTENSION= '' ";
assert_eq!(parse_fixed_string(r).unwrap(), "");
let r = b"XTENSION= ' ' ";
assert_eq!(parse_fixed_string(r).unwrap(), " ");
let r = b"XTENSION= '''' ";
assert_eq!(parse_fixed_string(r).unwrap(), "'");
let r = b"XTENSION= 'IMAGE ' ";
assert_eq!(parse_fixed_string(r).unwrap(), "IMAGE");
let r = b"XTENSION= 'looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong'";
assert_eq!(
parse_fixed_string(r).unwrap(),
"looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong"
);
let r = b"XTENSION= 'hello'/ok comment goes here ";
assert_eq!(parse_fixed_string(r).unwrap(), "hello");
let r = b"XTENSION= 'hello' / ok comment goes here ";
assert_eq!(parse_fixed_string(r).unwrap(), "hello");
let r = b"XTENSION= nope ";
assert!(parse_fixed_string(r).is_err());
let r = b"XTENSION= 'OK' nope ";
assert!(parse_fixed_string(r).is_err());
let r = b"XTENSION= 'nope ";
assert!(parse_fixed_string(r).is_err());
}
fn accumulate_naxis_value(record: &[u8], naxis: &mut Vec<usize>) -> Result<bool, FitsFormatError> {
if &record[..5] != b"NAXIS" {
return Ok(false); }
if &record[8..10] != b"= " {
return Err(FitsFormatError::MalformedNaxis);
}
let mut value = 0;
let mut i = 5;
while i < 8 {
if record[i] == b' ' {
break;
}
value *= 10;
match record[i] {
b'0' => {}
b'1' => {
value += 1;
}
b'2' => {
value += 2;
}
b'3' => {
value += 3;
}
b'4' => {
value += 4;
}
b'5' => {
value += 5;
}
b'6' => {
value += 6;
}
b'7' => {
value += 7;
}
b'8' => {
value += 8;
}
b'9' => {
value += 9;
}
other => {
return Err(FitsFormatError::ExpectedDigitNaxisHeader(other));
}
}
i += 1;
}
while i < 8 {
if record[i] != b' ' {
return Err(FitsFormatError::ExpectedSpaceNaxisHeader(record[i]));
}
i += 1;
}
if value != naxis.len() + 1 {
return Err(FitsFormatError::MisnumberedNaxis {
expected: naxis.len() + 1,
got: value,
});
}
let n = parse_fixed_int(record)?;
if n < 0 {
return Err(FitsFormatError::NegativeNaxisValue { value, n });
}
naxis.push(n as usize);
Ok(true)
}