///|
fn required_card(
header : Header,
index : Int,
keyword : String,
) -> Result[Card, FitsError] {
if index >= header.cards.length() {
return Err(MissingKeyword(keyword))
}
let card = header.cards[index]
if card.keyword != keyword {
return Err(InvalidRequiredOrder(index, keyword, card.keyword))
}
Ok(card)
}
///|
fn card_lexical_value(card : Card) -> Result[String, FitsError] {
match card.value {
Some(value) if value != "" => Ok(value)
_ => Err(InvalidCard(card.record_index, "\{card.keyword} requires a value"))
}
}
///|
fn parse_card_integer(card : Card) -> Result[Int, FitsError] {
let text = match card_lexical_value(card) {
Err(error) => return Err(error)
Ok(value) => value
}
let chars = text.to_array()
if chars.length() == 0 {
return Err(InvalidInteger(card.keyword, text))
}
let mut index = 0
let mut negative = false
if chars[0] == '-' || chars[0] == '+' {
negative = chars[0] == '-'
index = 1
}
if index == chars.length() {
return Err(InvalidInteger(card.keyword, text))
}
let mut value = 0
while index < chars.length() {
let ch = chars[index]
if ch < '0' || ch > '9' {
return Err(InvalidInteger(card.keyword, text))
}
let digit = ch.to_int() - '0'.to_int()
if value > (2147483647 - digit) / 10 {
return Err(SizeOverflow("integer keyword \{card.keyword}"))
}
value = value * 10 + digit
index = index + 1
}
Ok(if negative { -value } else { value })
}
///|
fn parse_card_logical(card : Card) -> Result[Bool, FitsError] {
let text = match card_lexical_value(card) {
Err(error) => return Err(error)
Ok(value) => value
}
match text {
"T" => Ok(true)
"F" => Ok(false)
_ => Err(InvalidLogical(card.keyword, text))
}
}
///|
fn parse_card_string(card : Card) -> Result[String, FitsError] {
let text = match card_lexical_value(card) {
Err(error) => return Err(error)
Ok(value) => value
}
let chars = text.to_array()
if chars.length() < 2 || chars[0] != '\'' {
return Err(InvalidString(card.keyword, text))
}
let out = StringBuilder()
let mut index = 1
while index < chars.length() {
if chars[index] == '\'' {
if index + 1 < chars.length() && chars[index + 1] == '\'' {
out.write_char('\'')
index = index + 2
continue
}
for trailing = index + 1
trailing < chars.length()
trailing = trailing + 1 {
if chars[trailing] != ' ' {
return Err(InvalidString(card.keyword, text))
}
}
return Ok(out.to_string().trim_end(chars=" ").to_owned())
}
out.write_char(chars[index])
index = index + 1
}
Err(InvalidString(card.keyword, text))
}
///|
fn checked_product(
left : Int,
right : Int,
context : String,
) -> Result[Int, FitsError] {
if left < 0 || right < 0 {
return Err(SizeOverflow(context))
}
if left != 0 && right > 2147483647 / left {
return Err(SizeOverflow(context))
}
Ok(left * right)
}
///|
fn checked_sum(
left : Int,
right : Int,
context : String,
) -> Result[Int, FitsError] {
if left < 0 || right < 0 || left > 2147483647 - right {
return Err(SizeOverflow(context))
}
Ok(left + right)
}
///|
fn checked_padded_block_length(length : Int) -> Result[Int, FitsError] {
if length > 2147483647 - (fits_block_size - 1) {
return Err(SizeOverflow("padded data unit"))
}
Ok(padded_block_length(length))
}
///|
fn bitpix_bytes(bitpix : Int) -> Result[Int, FitsError] {
match bitpix {
8 => Ok(1)
16 => Ok(2)
32 | -32 => Ok(4)
64 | -64 => Ok(8)
_ => Err(UnsupportedBitpix(bitpix))
}
}
///|
priv struct HduLayout {
kind : HduKind
image : ImageLayout?
data_length : Int
}
///|
fn analyze_hdu_header(
header : Header,
hdu_index : Int,
) -> Result[HduLayout, FitsError] {
let kind = if hdu_index == 0 {
let simple = match required_card(header, 0, "SIMPLE") {
Err(error) => return Err(error)
Ok(card) => card
}
match parse_card_logical(simple) {
Ok(true) => ()
Ok(false) =>
return Err(InvalidCard(simple.record_index, "SIMPLE must be T"))
Err(error) => return Err(error)
}
Primary
} else {
let xtension = match required_card(header, 0, "XTENSION") {
Err(error) => return Err(error)
Ok(card) =>
match parse_card_string(card) {
Err(error) => return Err(error)
Ok(value) => value
}
}
if xtension == "IMAGE" {
ImageExtension
} else {
OtherExtension(xtension)
}
}
let bitpix_card = match required_card(header, 1, "BITPIX") {
Err(error) => return Err(error)
Ok(card) => card
}
let bitpix = match parse_card_integer(bitpix_card) {
Err(error) => return Err(error)
Ok(value) => value
}
let bytes_per_value = match bitpix_bytes(bitpix) {
Err(error) => return Err(error)
Ok(value) => value
}
let naxis_card = match required_card(header, 2, "NAXIS") {
Err(error) => return Err(error)
Ok(card) => card
}
let naxis = match parse_card_integer(naxis_card) {
Err(error) => return Err(error)
Ok(value) => value
}
if naxis < 0 || naxis > 999 {
return Err(InvalidParameter("NAXIS", naxis))
}
let axes = []
let mut pixel_count = if naxis == 0 { 0 } else { 1 }
for axis = 1; axis <= naxis; axis = axis + 1 {
let keyword = "NAXIS\{axis}"
let card = match required_card(header, 2 + axis, keyword) {
Err(error) => return Err(error)
Ok(value) => value
}
let length = match parse_card_integer(card) {
Err(error) => return Err(error)
Ok(value) => value
}
if length < 0 {
return Err(InvalidAxis(axis, length))
}
axes.push(length)
pixel_count = match checked_product(pixel_count, length, "pixel count") {
Err(error) => return Err(error)
Ok(value) => value
}
}
let pcount_card = if hdu_index == 0 {
header.get("PCOUNT")
} else {
match required_card(header, 3 + naxis, "PCOUNT") {
Err(error) => return Err(error)
Ok(card) => Some(card)
}
}
let gcount_card = if hdu_index == 0 {
header.get("GCOUNT")
} else {
match required_card(header, 4 + naxis, "GCOUNT") {
Err(error) => return Err(error)
Ok(card) => Some(card)
}
}
let pcount = match pcount_card {
None => 0
Some(card) =>
match parse_card_integer(card) {
Err(error) => return Err(error)
Ok(value) => value
}
}
let gcount = match gcount_card {
None => 1
Some(card) =>
match parse_card_integer(card) {
Err(error) => return Err(error)
Ok(value) => value
}
}
if pcount < 0 {
return Err(InvalidParameter("PCOUNT", pcount))
}
if gcount < 1 {
return Err(InvalidParameter("GCOUNT", gcount))
}
let values_per_group = match
checked_sum(pixel_count, pcount, "values per group") {
Err(error) => return Err(error)
Ok(value) => value
}
let grouped_values = match
checked_product(values_per_group, gcount, "grouped value count") {
Err(error) => return Err(error)
Ok(value) => value
}
let data_length = match
checked_product(grouped_values, bytes_per_value, "data unit") {
Err(error) => return Err(error)
Ok(value) => value
}
let image = match kind {
Primary | ImageExtension =>
Some({ bitpix, axes, pixel_count, byte_length: data_length, })
OtherExtension(_) => None
}
Ok({ kind, image, data_length, })
}
///|
fn parse_hdu_layout(
data : Bytes,
header : Header,
hdu_index : Int,
) -> Result[Hdu, FitsError] {
let layout = match analyze_hdu_header(header, hdu_index) {
Err(error) => return Err(error)
Ok(value) => value
}
let padded_data_length = match
checked_padded_block_length(layout.data_length) {
Err(error) => return Err(error)
Ok(value) => value
}
if header.data_offset < 0 || header.data_offset > data.length() {
return Err(Truncated(header.data_offset, padded_data_length, 0))
}
let available = data.length() - header.data_offset
if padded_data_length > available {
return Err(Truncated(header.data_offset, padded_data_length, available))
}
Ok({
index: hdu_index,
kind: layout.kind,
header,
image: layout.image,
data_offset: header.data_offset,
data_length: layout.data_length,
next_hdu_offset: header.data_offset + padded_data_length,
})
}
///|
/// Parse and structurally validate every HDU in a complete FITS byte stream.
pub fn parse_fits(data : Bytes) -> Result[FitsFile, FitsError] {
if data.length() == 0 {
return Err(Truncated(0, fits_block_size, 0))
}
if data.length() % fits_block_size != 0 {
return Err(
Truncated(
data.length(),
fits_block_size - data.length() % fits_block_size,
0,
),
)
}
let hdus = []
let mut offset = 0
while offset < data.length() {
let header = match parse_header_at(data, offset) {
Err(error) => return Err(error)
Ok(value) => value
}
let hdu = match parse_hdu_layout(data, header, hdus.length()) {
Err(error) => return Err(error)
Ok(value) => value
}
if hdu.next_hdu_offset <= offset {
return Err(SizeOverflow("HDU traversal"))
}
offset = hdu.next_hdu_offset
hdus.push(hdu)
}
Ok({ hdus, })
}
///|
pub fn FitsFile::primary(self : FitsFile) -> Hdu? {
if self.hdus.length() == 0 {
None
} else {
Some(self.hdus[0])
}
}