///|
let fits_card_size : Int = 80
///|
let fits_block_size : Int = 2880
///|
fn is_fits_keyword_char(ch : Char) -> Bool {
(ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_'
}
///|
fn ascii_slice(
data : Bytes,
start : Int,
end : Int,
) -> Result[String, FitsError] {
let out = StringBuilder()
for index = start; index < end; index = index + 1 {
let byte = data[index]
let value = byte.to_int()
if value < 0x20 || value > 0x7e {
return Err(NonAscii(index, byte))
}
out.write_char(value.to_char().unwrap())
}
Ok(out.to_string())
}
///|
fn fits_trim(input : String) -> String {
input.trim(chars=" ").to_owned()
}
///|
fn fits_trim_end(input : String) -> String {
input.trim_end(chars=" ").to_owned()
}
///|
fn split_value_comment(input : String) -> (String, String?) {
let chars = input.to_array()
let mut quoted = false
let mut index = 0
while index < chars.length() {
if chars[index] == '\'' {
if quoted && index + 1 < chars.length() && chars[index + 1] == '\'' {
index = index + 2
continue
}
quoted = !quoted
} else if chars[index] == '/' && !quoted {
let value = StringBuilder()
for position = 0; position < index; position = position + 1 {
value.write_char(chars[position])
}
let comment = StringBuilder()
for position = index + 1
position < chars.length()
position = position + 1 {
comment.write_char(chars[position])
}
let comment_text = fits_trim(comment.to_string())
return (
fits_trim(value.to_string()),
if comment_text == "" {
None
} else {
Some(comment_text)
},
)
}
index = index + 1
}
(fits_trim(input), None)
}
///|
fn validate_keyword(
keyword : String,
record_index : Int,
) -> Result[Unit, FitsError] {
if keyword == "" {
return Ok(())
}
for ch in keyword {
if !is_fits_keyword_char(ch) {
return Err(InvalidKeyword(record_index, keyword))
}
}
Ok(())
}
///|
fn parse_card(
data : Bytes,
offset : Int,
record_index : Int,
) -> Result[Card, FitsError] {
let keyword = match ascii_slice(data, offset, offset + 8) {
Err(error) => return Err(error)
Ok(text) => fits_trim_end(text)
}
match validate_keyword(keyword, record_index) {
Err(error) => return Err(error)
Ok(_) => ()
}
let has_value = data[offset + 8] == b'=' && data[offset + 9] == b' '
if keyword == "END" {
if has_value {
return Err(
InvalidCard(record_index, "END must not have a value indicator"),
)
}
let trailing = match
ascii_slice(data, offset + 8, offset + fits_card_size) {
Err(error) => return Err(error)
Ok(value) => fits_trim(value)
}
if trailing != "" {
return Err(InvalidCard(record_index, "END must not have a comment"))
}
}
if has_value {
let field = match ascii_slice(data, offset + 10, offset + fits_card_size) {
Err(error) => return Err(error)
Ok(text) => text
}
let (value, comment) = split_value_comment(field)
Ok({ keyword, value: Some(value), comment, record_index, })
} else {
let text = match ascii_slice(data, offset + 8, offset + fits_card_size) {
Err(error) => return Err(error)
Ok(value) => fits_trim(value)
}
Ok({
keyword,
value: None,
comment: if text == "" {
None
} else {
Some(text)
},
record_index,
})
}
}
///|
fn padded_block_length(length : Int) -> Int {
(length + fits_block_size - 1) / fits_block_size * fits_block_size
}
///|
/// Parse a FITS header at byte zero.
pub fn parse_header(data : Bytes) -> Result[Header, FitsError] {
parse_header_at(data, 0)
}
///|
/// Parse a FITS header at an absolute byte offset.
pub fn parse_header_at(data : Bytes, start : Int) -> Result[Header, FitsError] {
if start < 0 || start > data.length() {
return Err(Truncated(start, fits_card_size, 0))
}
let cards = []
let mut record_index = 0
while true {
let offset = start + record_index * fits_card_size
if offset + fits_card_size > data.length() {
return Err(MissingEnd(start))
}
let card = match parse_card(data, offset, record_index) {
Err(error) => return Err(error)
Ok(value) => value
}
cards.push(card)
record_index = record_index + 1
if card.keyword == "END" {
let header_length = padded_block_length(record_index * fits_card_size)
let data_offset = start + header_length
if data_offset > data.length() {
return Err(Truncated(start, header_length, data.length() - start))
}
let content_end = start + record_index * fits_card_size
for index = content_end; index < data_offset; index = index + 1 {
if data[index] != b' ' {
return Err(InvalidPadding(index, data[index]))
}
}
return Ok({ cards, data_offset, })
}
} nobreak {
Err(MissingEnd(start))
}
}
///|
/// Return the first card with the requested keyword.
pub fn Header::get(self : Header, keyword : String) -> Card? {
for card in self.cards {
if card.keyword == keyword {
return Some(card)
}
}
None
}
///|
/// Return every card with the requested keyword in source order.
pub fn Header::get_all(self : Header, keyword : String) -> Array[Card] {
let found = []
for card in self.cards {
if card.keyword == keyword {
found.push(card)
}
}
found
}