use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use crate::block::padded_byte_len;
use crate::endian::{
read_f32_be, read_f64_be, read_i16_be, read_i32_be, read_i64_be, write_f32_be, write_f64_be,
write_i16_be, write_i32_be, write_i64_be,
};
use crate::error::{Error, Result};
use crate::hdu::{Hdu, HduInfo};
use crate::header::{serialize_header, Card};
use crate::value::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinaryColumnType {
Logical,
Bit,
Byte,
Short,
Int,
Long,
Float,
Double,
ComplexFloat,
ComplexDouble,
Ascii,
VarArrayP(char),
VarArrayQ(char),
}
#[derive(Debug, Clone, PartialEq)]
pub struct BinaryColumnDescriptor {
pub name: Option<String>,
pub repeat: usize,
pub col_type: BinaryColumnType,
pub byte_width: usize,
pub tdim: Option<Vec<usize>>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BinaryColumnData {
Logical(Vec<bool>),
Byte(Vec<u8>),
Short(Vec<i16>),
Int(Vec<i32>),
Long(Vec<i64>),
Float(Vec<f32>),
Double(Vec<f64>),
ComplexFloat(Vec<(f32, f32)>),
ComplexDouble(Vec<(f64, f64)>),
Ascii(Vec<String>),
Bit(Vec<Vec<u8>>),
VarByte(Vec<Vec<u8>>),
VarShort(Vec<Vec<i16>>),
VarInt(Vec<Vec<i32>>),
VarLong(Vec<Vec<i64>>),
VarFloat(Vec<Vec<f32>>),
VarDouble(Vec<Vec<f64>>),
}
pub fn binary_type_byte_size(col_type: &BinaryColumnType) -> usize {
match col_type {
BinaryColumnType::Logical => 1,
BinaryColumnType::Bit => 0,
BinaryColumnType::Byte => 1,
BinaryColumnType::Short => 2,
BinaryColumnType::Int => 4,
BinaryColumnType::Long => 8,
BinaryColumnType::Float => 4,
BinaryColumnType::Double => 8,
BinaryColumnType::ComplexFloat => 8,
BinaryColumnType::ComplexDouble => 16,
BinaryColumnType::Ascii => 1,
BinaryColumnType::VarArrayP(_) => 8,
BinaryColumnType::VarArrayQ(_) => 16,
}
}
pub fn parse_tform_binary(s: &str) -> Result<(usize, BinaryColumnType)> {
let s = s.trim();
if s.is_empty() {
return Err(Error::InvalidValue);
}
let s = if let Some(paren) = s.find('(') {
&s[..paren]
} else {
s
};
if s.len() >= 2 {
let bytes = s.as_bytes();
let last = bytes[s.len() - 1];
let second_last = bytes[s.len() - 2];
if second_last == b'P' || second_last == b'Q' {
match last {
b'L' | b'X' | b'B' | b'I' | b'J' | b'K' | b'E' | b'D' | b'C' | b'M' | b'A' => {}
_ => return Err(Error::InvalidValue),
}
let repeat_str = &s[..s.len() - 2];
let repeat = if repeat_str.is_empty() {
1
} else {
repeat_str
.parse::<usize>()
.map_err(|_| Error::InvalidValue)?
};
let elem_char = last as char;
let col_type = if second_last == b'P' {
BinaryColumnType::VarArrayP(elem_char)
} else {
BinaryColumnType::VarArrayQ(elem_char)
};
return Ok((repeat, col_type));
}
}
let type_char = s.as_bytes()[s.len() - 1];
let repeat_str = &s[..s.len() - 1];
let repeat = if repeat_str.is_empty() {
1
} else {
repeat_str
.parse::<usize>()
.map_err(|_| Error::InvalidValue)?
};
let col_type = match type_char {
b'L' => BinaryColumnType::Logical,
b'X' => BinaryColumnType::Bit,
b'B' => BinaryColumnType::Byte,
b'I' => BinaryColumnType::Short,
b'J' => BinaryColumnType::Int,
b'K' => BinaryColumnType::Long,
b'E' => BinaryColumnType::Float,
b'D' => BinaryColumnType::Double,
b'C' => BinaryColumnType::ComplexFloat,
b'M' => BinaryColumnType::ComplexDouble,
b'A' => BinaryColumnType::Ascii,
_ => return Err(Error::InvalidValue),
};
Ok((repeat, col_type))
}
fn compute_byte_width(repeat: usize, col_type: &BinaryColumnType) -> usize {
match col_type {
BinaryColumnType::Bit => repeat.div_ceil(8),
BinaryColumnType::VarArrayP(_) => 8 * repeat,
BinaryColumnType::VarArrayQ(_) => 16 * repeat,
_ => repeat * binary_type_byte_size(col_type),
}
}
fn make_keyword(name: &str) -> [u8; 8] {
let mut k = [b' '; 8];
let bytes = name.as_bytes();
let len = bytes.len().min(8);
k[..len].copy_from_slice(&bytes[..len]);
k
}
fn card_float_value(cards: &[Card], keyword: &str) -> Option<f64> {
cards.iter().find_map(|c| {
if c.keyword_str() == keyword {
match &c.value {
Some(Value::Float(f)) => Some(*f),
Some(Value::Integer(n)) => Some(*n as f64),
_ => None,
}
} else {
None
}
})
}
fn card_string_value(cards: &[Card], keyword: &str) -> Option<String> {
cards.iter().find_map(|c| {
if c.keyword_str() == keyword {
match &c.value {
Some(Value::String(s)) => Some(s.trim().into()),
_ => None,
}
} else {
None
}
})
}
pub fn parse_tdim(s: &str) -> Option<Vec<usize>> {
let s = s.trim();
if s.is_empty() {
return None;
}
let s = s.strip_prefix('(')?.strip_suffix(')')?;
let dims: Option<Vec<usize>> = s
.split(',')
.map(|part| part.trim().parse::<usize>().ok())
.collect();
let dims = dims?;
if dims.is_empty() {
return None;
}
Some(dims)
}
fn card_int_value(cards: &[Card], keyword: &str) -> Option<i64> {
cards.iter().find_map(|c| {
if c.keyword_str() == keyword {
match &c.value {
Some(Value::Integer(v)) => Some(*v),
_ => None,
}
} else {
None
}
})
}
pub fn parse_binary_table_columns(
cards: &[Card],
tfields: usize,
) -> Result<Vec<BinaryColumnDescriptor>> {
let mut columns = Vec::with_capacity(tfields);
for i in 1..=tfields {
let tform_key = alloc::format!("TFORM{}", i);
let tform_str =
card_string_value(cards, &tform_key).ok_or(Error::MissingKeyword("TFORMn"))?;
let (repeat, col_type) = parse_tform_binary(&tform_str)?;
let ttype_key = alloc::format!("TTYPE{}", i);
let name = card_string_value(cards, &ttype_key);
let tdim_key = alloc::format!("TDIM{}", i);
let tdim = card_string_value(cards, &tdim_key).and_then(|s| parse_tdim(&s));
let byte_width = compute_byte_width(repeat, &col_type);
columns.push(BinaryColumnDescriptor {
name,
repeat,
col_type,
byte_width,
tdim,
});
}
Ok(columns)
}
fn extract_table_info(
fits_data: &[u8],
hdu: &Hdu,
) -> Result<(usize, usize, Vec<BinaryColumnDescriptor>)> {
let (naxis1, naxis2, tfields) = match &hdu.info {
HduInfo::BinaryTable {
naxis1,
naxis2,
tfields,
..
} => (*naxis1, *naxis2, *tfields),
_ => return Err(Error::InvalidHeader("not a binary table HDU")),
};
if hdu.data_start + naxis1 * naxis2 > fits_data.len() {
return Err(Error::UnexpectedEof);
}
let columns = parse_binary_table_columns(&hdu.cards, tfields)?;
Ok((naxis1, naxis2, columns))
}
fn column_offsets(columns: &[BinaryColumnDescriptor]) -> Vec<usize> {
let mut offsets = Vec::with_capacity(columns.len());
let mut offset = 0usize;
for col in columns {
offsets.push(offset);
offset += col.byte_width;
}
offsets
}
pub fn read_binary_column(
fits_data: &[u8],
hdu: &Hdu,
col_index: usize,
) -> Result<BinaryColumnData> {
let (naxis1, naxis2, columns) = extract_table_info(fits_data, hdu)?;
if col_index >= columns.len() {
return Err(Error::InvalidValue);
}
let offsets = column_offsets(&columns);
let col = &columns[col_index];
let col_offset = offsets[col_index];
let data_start = hdu.data_start;
read_column_cells(fits_data, data_start, naxis1, naxis2, col, col_offset)
}
pub fn read_binary_column_range(
fits_data: &[u8],
hdu: &Hdu,
col_index: usize,
start_row: usize,
num_rows: usize,
) -> Result<BinaryColumnData> {
let (naxis1, naxis2, columns) = extract_table_info(fits_data, hdu)?;
if col_index >= columns.len() {
return Err(Error::InvalidValue);
}
if start_row + num_rows > naxis2 {
return Err(Error::InvalidValue);
}
let offsets = column_offsets(&columns);
let col = &columns[col_index];
let col_offset = offsets[col_index];
let row_data_start = hdu.data_start + start_row * naxis1;
read_column_cells(fits_data, row_data_start, naxis1, num_rows, col, col_offset)
}
pub fn write_binary_column(
fits_data: &mut [u8],
hdu: &Hdu,
col_index: usize,
data: &BinaryColumnData,
) -> Result<()> {
let (naxis1, naxis2, columns) = extract_table_info(fits_data, hdu)?;
if col_index >= columns.len() {
return Err(Error::InvalidValue);
}
let offsets = column_offsets(&columns);
let col = &columns[col_index];
let col_offset = offsets[col_index];
let data_start = hdu.data_start;
for row in 0..naxis2 {
let cell_bytes = serialize_binary_column_value(&col.col_type, col.repeat, data, row)?;
let base = data_start + row * naxis1 + col_offset;
fits_data[base..base + cell_bytes.len()].copy_from_slice(&cell_bytes);
}
Ok(())
}
fn read_column_cells(
fits_data: &[u8],
data_start: usize,
naxis1: usize,
naxis2: usize,
col: &BinaryColumnDescriptor,
col_offset: usize,
) -> Result<BinaryColumnData> {
match col.col_type {
BinaryColumnType::VarArrayP(_) | BinaryColumnType::VarArrayQ(_) => Err(Error::InvalidValue),
BinaryColumnType::Logical => {
let mut values = Vec::with_capacity(naxis2 * col.repeat);
for row in 0..naxis2 {
let base = data_start + row * naxis1 + col_offset;
for r in 0..col.repeat {
let b = fits_data[base + r];
values.push(b == b'T');
}
}
Ok(BinaryColumnData::Logical(values))
}
BinaryColumnType::Byte => {
let mut values = Vec::with_capacity(naxis2 * col.repeat);
for row in 0..naxis2 {
let base = data_start + row * naxis1 + col_offset;
for r in 0..col.repeat {
values.push(fits_data[base + r]);
}
}
Ok(BinaryColumnData::Byte(values))
}
BinaryColumnType::Short => {
let mut values = Vec::with_capacity(naxis2 * col.repeat);
for row in 0..naxis2 {
let base = data_start + row * naxis1 + col_offset;
for r in 0..col.repeat {
let off = base + r * 2;
values.push(read_i16_be(&fits_data[off..]));
}
}
Ok(BinaryColumnData::Short(values))
}
BinaryColumnType::Int => {
let mut values = Vec::with_capacity(naxis2 * col.repeat);
for row in 0..naxis2 {
let base = data_start + row * naxis1 + col_offset;
for r in 0..col.repeat {
let off = base + r * 4;
values.push(read_i32_be(&fits_data[off..]));
}
}
Ok(BinaryColumnData::Int(values))
}
BinaryColumnType::Long => {
let mut values = Vec::with_capacity(naxis2 * col.repeat);
for row in 0..naxis2 {
let base = data_start + row * naxis1 + col_offset;
for r in 0..col.repeat {
let off = base + r * 8;
values.push(read_i64_be(&fits_data[off..]));
}
}
Ok(BinaryColumnData::Long(values))
}
BinaryColumnType::Float => {
let mut values = Vec::with_capacity(naxis2 * col.repeat);
for row in 0..naxis2 {
let base = data_start + row * naxis1 + col_offset;
for r in 0..col.repeat {
let off = base + r * 4;
values.push(read_f32_be(&fits_data[off..]));
}
}
Ok(BinaryColumnData::Float(values))
}
BinaryColumnType::Double => {
let mut values = Vec::with_capacity(naxis2 * col.repeat);
for row in 0..naxis2 {
let base = data_start + row * naxis1 + col_offset;
for r in 0..col.repeat {
let off = base + r * 8;
values.push(read_f64_be(&fits_data[off..]));
}
}
Ok(BinaryColumnData::Double(values))
}
BinaryColumnType::ComplexFloat => {
let mut values = Vec::with_capacity(naxis2 * col.repeat);
for row in 0..naxis2 {
let base = data_start + row * naxis1 + col_offset;
for r in 0..col.repeat {
let off = base + r * 8;
let re = read_f32_be(&fits_data[off..]);
let im = read_f32_be(&fits_data[off + 4..]);
values.push((re, im));
}
}
Ok(BinaryColumnData::ComplexFloat(values))
}
BinaryColumnType::ComplexDouble => {
let mut values = Vec::with_capacity(naxis2 * col.repeat);
for row in 0..naxis2 {
let base = data_start + row * naxis1 + col_offset;
for r in 0..col.repeat {
let off = base + r * 16;
let re = read_f64_be(&fits_data[off..]);
let im = read_f64_be(&fits_data[off + 8..]);
values.push((re, im));
}
}
Ok(BinaryColumnData::ComplexDouble(values))
}
BinaryColumnType::Ascii => {
let mut values = Vec::with_capacity(naxis2);
for row in 0..naxis2 {
let base = data_start + row * naxis1 + col_offset;
let bytes = &fits_data[base..base + col.repeat];
let s = core::str::from_utf8(bytes)
.map_err(|_| Error::InvalidValue)?
.trim_end()
.into();
values.push(s);
}
Ok(BinaryColumnData::Ascii(values))
}
BinaryColumnType::Bit => {
let bytes_per_row = col.repeat.div_ceil(8);
let mut values = Vec::with_capacity(naxis2);
for row in 0..naxis2 {
let base = data_start + row * naxis1 + col_offset;
let bytes = fits_data[base..base + bytes_per_row].to_vec();
values.push(bytes);
}
Ok(BinaryColumnData::Bit(values))
}
}
}
pub fn read_binary_row(
fits_data: &[u8],
hdu: &Hdu,
row_index: usize,
) -> Result<Vec<BinaryColumnData>> {
let (naxis1, naxis2, columns) = extract_table_info(fits_data, hdu)?;
if row_index >= naxis2 {
return Err(Error::InvalidValue);
}
let offsets = column_offsets(&columns);
let data_start = hdu.data_start;
let mut result = Vec::with_capacity(columns.len());
for (i, col) in columns.iter().enumerate() {
let col_offset = offsets[i];
let cell = read_column_cells(
fits_data,
data_start,
naxis1,
1,
col,
col_offset + row_index * naxis1,
)?;
result.push(cell);
}
Ok(result)
}
pub fn extract_column_scaling(cards: &[Card], col_number: usize) -> (f64, f64) {
let tscal_kw = alloc::format!("TSCAL{}", col_number);
let tzero_kw = alloc::format!("TZERO{}", col_number);
let tscal = card_float_value(cards, &tscal_kw).unwrap_or(1.0);
let tzero = card_float_value(cards, &tzero_kw).unwrap_or(0.0);
(tscal, tzero)
}
pub fn apply_column_scaling(data: &BinaryColumnData, tscal: f64, tzero: f64) -> Vec<f64> {
match data {
BinaryColumnData::Byte(v) => v.iter().map(|&x| tzero + tscal * (x as f64)).collect(),
BinaryColumnData::Short(v) => v.iter().map(|&x| tzero + tscal * (x as f64)).collect(),
BinaryColumnData::Int(v) => v.iter().map(|&x| tzero + tscal * (x as f64)).collect(),
BinaryColumnData::Long(v) => v.iter().map(|&x| tzero + tscal * (x as f64)).collect(),
BinaryColumnData::Float(v) => v.iter().map(|&x| tzero + tscal * (x as f64)).collect(),
BinaryColumnData::Double(v) => v.iter().map(|&x| tzero + tscal * x).collect(),
BinaryColumnData::Logical(v) => v
.iter()
.map(|&x| tzero + tscal * (x as u8 as f64))
.collect(),
BinaryColumnData::ComplexFloat(_)
| BinaryColumnData::ComplexDouble(_)
| BinaryColumnData::Ascii(_)
| BinaryColumnData::Bit(_)
| BinaryColumnData::VarByte(_)
| BinaryColumnData::VarShort(_)
| BinaryColumnData::VarInt(_)
| BinaryColumnData::VarLong(_)
| BinaryColumnData::VarFloat(_)
| BinaryColumnData::VarDouble(_) => Vec::new(),
}
}
pub fn read_binary_column_physical(
fits_data: &[u8],
hdu: &Hdu,
col_index: usize,
) -> Result<Vec<f64>> {
let raw = read_binary_column(fits_data, hdu, col_index)?;
let (tscal, tzero) = extract_column_scaling(&hdu.cards, col_index + 1);
Ok(apply_column_scaling(&raw, tscal, tzero))
}
fn read_p_descriptor(data: &[u8]) -> (usize, usize) {
let count = read_i32_be(data) as u32 as usize;
let offset = read_i32_be(&data[4..]) as u32 as usize;
(count, offset)
}
fn read_q_descriptor(data: &[u8]) -> (usize, usize) {
let count = read_i64_be(data) as u64 as usize;
let offset = read_i64_be(&data[8..]) as u64 as usize;
(count, offset)
}
pub fn read_binary_column_vla(
fits_data: &[u8],
hdu: &Hdu,
col_index: usize,
) -> Result<BinaryColumnData> {
let (naxis1, naxis2, pcount) = match &hdu.info {
HduInfo::BinaryTable {
naxis1,
naxis2,
pcount,
..
} => (*naxis1, *naxis2, *pcount),
_ => return Err(Error::InvalidHeader("not a binary table HDU")),
};
let columns = parse_binary_table_columns(
&hdu.cards,
match &hdu.info {
HduInfo::BinaryTable { tfields, .. } => *tfields,
_ => return Err(Error::InvalidHeader("not a binary table HDU")),
},
)?;
if col_index >= columns.len() {
return Err(Error::InvalidValue);
}
let offsets = column_offsets(&columns);
let col = &columns[col_index];
let col_offset = offsets[col_index];
let data_start = hdu.data_start;
let (elem_type, is_q) = match col.col_type {
BinaryColumnType::VarArrayP(c) => (c, false),
BinaryColumnType::VarArrayQ(c) => (c, true),
_ => return Err(Error::InvalidValue),
};
let theap = card_int_value(&hdu.cards, "THEAP")
.map(|v| v as usize)
.unwrap_or(naxis1 * naxis2);
let heap_start = data_start + theap;
if heap_start + pcount > fits_data.len() {
return Err(Error::UnexpectedEof);
}
let elem_size = match elem_type {
'B' | 'L' | 'A' => 1,
'I' => 2,
'J' | 'E' => 4,
'K' | 'D' => 8,
_ => return Err(Error::InvalidValue),
};
match elem_type {
'B' => {
let mut rows = Vec::with_capacity(naxis2);
for row in 0..naxis2 {
let desc_pos = data_start + row * naxis1 + col_offset;
let (count, offset) = if is_q {
read_q_descriptor(&fits_data[desc_pos..])
} else {
read_p_descriptor(&fits_data[desc_pos..])
};
let start = heap_start + offset;
let end = start + count * elem_size;
if end > fits_data.len() {
return Err(Error::UnexpectedEof);
}
rows.push(fits_data[start..end].to_vec());
}
Ok(BinaryColumnData::VarByte(rows))
}
'I' => {
let mut rows = Vec::with_capacity(naxis2);
for row in 0..naxis2 {
let desc_pos = data_start + row * naxis1 + col_offset;
let (count, offset) = if is_q {
read_q_descriptor(&fits_data[desc_pos..])
} else {
read_p_descriptor(&fits_data[desc_pos..])
};
let start = heap_start + offset;
if start + count * elem_size > fits_data.len() {
return Err(Error::UnexpectedEof);
}
let mut vals = Vec::with_capacity(count);
for i in 0..count {
vals.push(read_i16_be(&fits_data[start + i * 2..]));
}
rows.push(vals);
}
Ok(BinaryColumnData::VarShort(rows))
}
'J' => {
let mut rows = Vec::with_capacity(naxis2);
for row in 0..naxis2 {
let desc_pos = data_start + row * naxis1 + col_offset;
let (count, offset) = if is_q {
read_q_descriptor(&fits_data[desc_pos..])
} else {
read_p_descriptor(&fits_data[desc_pos..])
};
let start = heap_start + offset;
if start + count * elem_size > fits_data.len() {
return Err(Error::UnexpectedEof);
}
let mut vals = Vec::with_capacity(count);
for i in 0..count {
vals.push(read_i32_be(&fits_data[start + i * 4..]));
}
rows.push(vals);
}
Ok(BinaryColumnData::VarInt(rows))
}
'K' => {
let mut rows = Vec::with_capacity(naxis2);
for row in 0..naxis2 {
let desc_pos = data_start + row * naxis1 + col_offset;
let (count, offset) = if is_q {
read_q_descriptor(&fits_data[desc_pos..])
} else {
read_p_descriptor(&fits_data[desc_pos..])
};
let start = heap_start + offset;
if start + count * elem_size > fits_data.len() {
return Err(Error::UnexpectedEof);
}
let mut vals = Vec::with_capacity(count);
for i in 0..count {
vals.push(read_i64_be(&fits_data[start + i * 8..]));
}
rows.push(vals);
}
Ok(BinaryColumnData::VarLong(rows))
}
'E' => {
let mut rows = Vec::with_capacity(naxis2);
for row in 0..naxis2 {
let desc_pos = data_start + row * naxis1 + col_offset;
let (count, offset) = if is_q {
read_q_descriptor(&fits_data[desc_pos..])
} else {
read_p_descriptor(&fits_data[desc_pos..])
};
let start = heap_start + offset;
if start + count * elem_size > fits_data.len() {
return Err(Error::UnexpectedEof);
}
let mut vals = Vec::with_capacity(count);
for i in 0..count {
vals.push(read_f32_be(&fits_data[start + i * 4..]));
}
rows.push(vals);
}
Ok(BinaryColumnData::VarFloat(rows))
}
'D' => {
let mut rows = Vec::with_capacity(naxis2);
for row in 0..naxis2 {
let desc_pos = data_start + row * naxis1 + col_offset;
let (count, offset) = if is_q {
read_q_descriptor(&fits_data[desc_pos..])
} else {
read_p_descriptor(&fits_data[desc_pos..])
};
let start = heap_start + offset;
if start + count * elem_size > fits_data.len() {
return Err(Error::UnexpectedEof);
}
let mut vals = Vec::with_capacity(count);
for i in 0..count {
vals.push(read_f64_be(&fits_data[start + i * 8..]));
}
rows.push(vals);
}
Ok(BinaryColumnData::VarDouble(rows))
}
_ => Err(Error::InvalidValue),
}
}
pub fn serialize_binary_column_value(
col_type: &BinaryColumnType,
repeat: usize,
data: &BinaryColumnData,
row_index: usize,
) -> Result<Vec<u8>> {
match (col_type, data) {
(BinaryColumnType::Logical, BinaryColumnData::Logical(vals)) => {
let start = row_index * repeat;
let mut out = vec![0u8; repeat];
for i in 0..repeat {
out[i] = if vals[start + i] { b'T' } else { b'F' };
}
Ok(out)
}
(BinaryColumnType::Byte, BinaryColumnData::Byte(vals)) => {
let start = row_index * repeat;
Ok(vals[start..start + repeat].to_vec())
}
(BinaryColumnType::Short, BinaryColumnData::Short(vals)) => {
let start = row_index * repeat;
let mut out = vec![0u8; repeat * 2];
for i in 0..repeat {
write_i16_be(&mut out[i * 2..], vals[start + i]);
}
Ok(out)
}
(BinaryColumnType::Int, BinaryColumnData::Int(vals)) => {
let start = row_index * repeat;
let mut out = vec![0u8; repeat * 4];
for i in 0..repeat {
write_i32_be(&mut out[i * 4..], vals[start + i]);
}
Ok(out)
}
(BinaryColumnType::Long, BinaryColumnData::Long(vals)) => {
let start = row_index * repeat;
let mut out = vec![0u8; repeat * 8];
for i in 0..repeat {
write_i64_be(&mut out[i * 8..], vals[start + i]);
}
Ok(out)
}
(BinaryColumnType::Float, BinaryColumnData::Float(vals)) => {
let start = row_index * repeat;
let mut out = vec![0u8; repeat * 4];
for i in 0..repeat {
write_f32_be(&mut out[i * 4..], vals[start + i]);
}
Ok(out)
}
(BinaryColumnType::Double, BinaryColumnData::Double(vals)) => {
let start = row_index * repeat;
let mut out = vec![0u8; repeat * 8];
for i in 0..repeat {
write_f64_be(&mut out[i * 8..], vals[start + i]);
}
Ok(out)
}
(BinaryColumnType::ComplexFloat, BinaryColumnData::ComplexFloat(vals)) => {
let start = row_index * repeat;
let mut out = vec![0u8; repeat * 8];
for i in 0..repeat {
let (re, im) = vals[start + i];
write_f32_be(&mut out[i * 8..], re);
write_f32_be(&mut out[i * 8 + 4..], im);
}
Ok(out)
}
(BinaryColumnType::ComplexDouble, BinaryColumnData::ComplexDouble(vals)) => {
let start = row_index * repeat;
let mut out = vec![0u8; repeat * 16];
for i in 0..repeat {
let (re, im) = vals[start + i];
write_f64_be(&mut out[i * 16..], re);
write_f64_be(&mut out[i * 16 + 8..], im);
}
Ok(out)
}
(BinaryColumnType::Ascii, BinaryColumnData::Ascii(vals)) => {
let mut out = vec![b' '; repeat];
let s = vals[row_index].as_bytes();
let len = s.len().min(repeat);
out[..len].copy_from_slice(&s[..len]);
Ok(out)
}
(BinaryColumnType::Bit, BinaryColumnData::Bit(vals)) => Ok(vals[row_index].clone()),
_ => Err(Error::InvalidValue),
}
}
fn tform_string(repeat: usize, col_type: &BinaryColumnType) -> String {
let ch = match col_type {
BinaryColumnType::Logical => 'L',
BinaryColumnType::Bit => 'X',
BinaryColumnType::Byte => 'B',
BinaryColumnType::Short => 'I',
BinaryColumnType::Int => 'J',
BinaryColumnType::Long => 'K',
BinaryColumnType::Float => 'E',
BinaryColumnType::Double => 'D',
BinaryColumnType::ComplexFloat => 'C',
BinaryColumnType::ComplexDouble => 'M',
BinaryColumnType::Ascii => 'A',
BinaryColumnType::VarArrayP(elem) => {
return alloc::format!("{}P{}", repeat, elem);
}
BinaryColumnType::VarArrayQ(elem) => {
return alloc::format!("{}Q{}", repeat, elem);
}
};
alloc::format!("{}{}", repeat, ch)
}
fn make_card(keyword: &str, value: Value) -> Card {
Card {
keyword: make_keyword(keyword),
value: Some(value),
comment: None,
}
}
pub fn build_binary_table_cards(
columns: &[BinaryColumnDescriptor],
naxis2: usize,
pcount: usize,
) -> Result<Vec<Card>> {
let naxis1: usize = columns.iter().map(|c| c.byte_width).sum();
let tfields = columns.len();
let mut cards = vec![
make_card("XTENSION", Value::String(String::from("BINTABLE"))),
make_card("BITPIX", Value::Integer(8)),
make_card("NAXIS", Value::Integer(2)),
make_card("NAXIS1", Value::Integer(naxis1 as i64)),
make_card("NAXIS2", Value::Integer(naxis2 as i64)),
make_card("PCOUNT", Value::Integer(pcount as i64)),
make_card("GCOUNT", Value::Integer(1)),
make_card("TFIELDS", Value::Integer(tfields as i64)),
];
for (i, col) in columns.iter().enumerate() {
let n = i + 1;
let tform = tform_string(col.repeat, &col.col_type);
let tform_kw = alloc::format!("TFORM{}", n);
cards.push(make_card(&tform_kw, Value::String(tform)));
if let Some(ref name) = col.name {
let ttype_kw = alloc::format!("TTYPE{}", n);
cards.push(make_card(&ttype_kw, Value::String(name.clone())));
}
if let Some(ref dims) = col.tdim {
let tdim_kw = alloc::format!("TDIM{}", n);
let dim_strs: Vec<String> = dims.iter().map(|d| alloc::format!("{}", d)).collect();
let tdim_val = alloc::format!("({})", dim_strs.join(","));
cards.push(make_card(&tdim_kw, Value::String(tdim_val)));
}
}
Ok(cards)
}
pub fn serialize_binary_table(
columns: &[BinaryColumnDescriptor],
col_data: &[BinaryColumnData],
naxis2: usize,
) -> Result<Vec<u8>> {
if columns.len() != col_data.len() {
return Err(Error::InvalidValue);
}
let naxis1: usize = columns.iter().map(|c| c.byte_width).sum();
let raw_len = naxis1 * naxis2;
let padded_len = padded_byte_len(raw_len);
let mut buf = vec![0u8; padded_len];
for row in 0..naxis2 {
let mut col_offset = 0usize;
for (col_idx, col) in columns.iter().enumerate() {
let cell_bytes =
serialize_binary_column_value(&col.col_type, col.repeat, &col_data[col_idx], row)?;
let dest_start = row * naxis1 + col_offset;
buf[dest_start..dest_start + cell_bytes.len()].copy_from_slice(&cell_bytes);
col_offset += col.byte_width;
}
}
Ok(buf)
}
pub fn serialize_binary_table_hdu(
columns: &[BinaryColumnDescriptor],
col_data: &[BinaryColumnData],
naxis2: usize,
) -> Result<Vec<u8>> {
let cards = build_binary_table_cards(columns, naxis2, 0)?;
let header_bytes = serialize_header(&cards)?;
let data_bytes = serialize_binary_table(columns, col_data, naxis2)?;
let mut result = Vec::with_capacity(header_bytes.len() + data_bytes.len());
result.extend_from_slice(&header_bytes);
result.extend_from_slice(&data_bytes);
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::{padded_byte_len, BLOCK_SIZE};
use crate::header::serialize_header;
use alloc::string::String;
use alloc::vec;
#[test]
fn parse_tform_single_int() {
let (repeat, col_type) = parse_tform_binary("1J").unwrap();
assert_eq!(repeat, 1);
assert_eq!(col_type, BinaryColumnType::Int);
}
#[test]
fn parse_tform_no_repeat_prefix() {
let (repeat, col_type) = parse_tform_binary("J").unwrap();
assert_eq!(repeat, 1);
assert_eq!(col_type, BinaryColumnType::Int);
}
#[test]
fn parse_tform_ten_floats() {
let (repeat, col_type) = parse_tform_binary("10E").unwrap();
assert_eq!(repeat, 10);
assert_eq!(col_type, BinaryColumnType::Float);
}
#[test]
fn parse_tform_ascii() {
let (repeat, col_type) = parse_tform_binary("20A").unwrap();
assert_eq!(repeat, 20);
assert_eq!(col_type, BinaryColumnType::Ascii);
}
#[test]
fn parse_tform_logical() {
let (repeat, col_type) = parse_tform_binary("1L").unwrap();
assert_eq!(repeat, 1);
assert_eq!(col_type, BinaryColumnType::Logical);
}
#[test]
fn parse_tform_bit() {
let (repeat, col_type) = parse_tform_binary("1024X").unwrap();
assert_eq!(repeat, 1024);
assert_eq!(col_type, BinaryColumnType::Bit);
}
#[test]
fn parse_tform_double() {
let (repeat, col_type) = parse_tform_binary("1D").unwrap();
assert_eq!(repeat, 1);
assert_eq!(col_type, BinaryColumnType::Double);
}
#[test]
fn parse_tform_short() {
let (repeat, col_type) = parse_tform_binary("3I").unwrap();
assert_eq!(repeat, 3);
assert_eq!(col_type, BinaryColumnType::Short);
}
#[test]
fn parse_tform_long() {
let (repeat, col_type) = parse_tform_binary("1K").unwrap();
assert_eq!(repeat, 1);
assert_eq!(col_type, BinaryColumnType::Long);
}
#[test]
fn parse_tform_byte() {
let (repeat, col_type) = parse_tform_binary("5B").unwrap();
assert_eq!(repeat, 5);
assert_eq!(col_type, BinaryColumnType::Byte);
}
#[test]
fn parse_tform_complex_float() {
let (repeat, col_type) = parse_tform_binary("2C").unwrap();
assert_eq!(repeat, 2);
assert_eq!(col_type, BinaryColumnType::ComplexFloat);
}
#[test]
fn parse_tform_complex_double() {
let (repeat, col_type) = parse_tform_binary("1M").unwrap();
assert_eq!(repeat, 1);
assert_eq!(col_type, BinaryColumnType::ComplexDouble);
}
#[test]
fn parse_tform_invalid_type() {
assert!(parse_tform_binary("1Z").is_err());
}
#[test]
fn parse_tform_empty() {
assert!(parse_tform_binary("").is_err());
}
#[test]
fn parse_tform_whitespace_trimmed() {
let (repeat, col_type) = parse_tform_binary(" 1J ").unwrap();
assert_eq!(repeat, 1);
assert_eq!(col_type, BinaryColumnType::Int);
}
#[test]
fn byte_sizes() {
assert_eq!(binary_type_byte_size(&BinaryColumnType::Logical), 1);
assert_eq!(binary_type_byte_size(&BinaryColumnType::Byte), 1);
assert_eq!(binary_type_byte_size(&BinaryColumnType::Short), 2);
assert_eq!(binary_type_byte_size(&BinaryColumnType::Int), 4);
assert_eq!(binary_type_byte_size(&BinaryColumnType::Long), 8);
assert_eq!(binary_type_byte_size(&BinaryColumnType::Float), 4);
assert_eq!(binary_type_byte_size(&BinaryColumnType::Double), 8);
assert_eq!(binary_type_byte_size(&BinaryColumnType::ComplexFloat), 8);
assert_eq!(binary_type_byte_size(&BinaryColumnType::ComplexDouble), 16);
assert_eq!(binary_type_byte_size(&BinaryColumnType::Ascii), 1);
assert_eq!(binary_type_byte_size(&BinaryColumnType::Bit), 0);
}
#[test]
fn byte_width_int() {
assert_eq!(compute_byte_width(1, &BinaryColumnType::Int), 4);
assert_eq!(compute_byte_width(3, &BinaryColumnType::Int), 12);
}
#[test]
fn byte_width_bit() {
assert_eq!(compute_byte_width(1, &BinaryColumnType::Bit), 1);
assert_eq!(compute_byte_width(8, &BinaryColumnType::Bit), 1);
assert_eq!(compute_byte_width(9, &BinaryColumnType::Bit), 2);
assert_eq!(compute_byte_width(1024, &BinaryColumnType::Bit), 128);
}
#[test]
fn byte_width_ten_floats() {
assert_eq!(compute_byte_width(10, &BinaryColumnType::Float), 40);
}
fn card_val(keyword: &str, value: Value) -> Card {
Card {
keyword: make_keyword(keyword),
value: Some(value),
comment: None,
}
}
fn build_bintable_hdu(header_cards: &[Card], raw_data: &[u8]) -> Vec<u8> {
let header = serialize_header(header_cards).unwrap();
let padded_data = padded_byte_len(raw_data.len());
let mut result = Vec::with_capacity(header.len() + padded_data);
result.extend_from_slice(&header);
let data_offset = result.len();
result.resize(data_offset + padded_data, 0u8);
result[data_offset..data_offset + raw_data.len()].copy_from_slice(raw_data);
result
}
fn make_bintable_header(
naxis1: usize,
naxis2: usize,
tfields: usize,
tforms: &[&str],
ttype_names: &[Option<&str>],
) -> Vec<Card> {
let mut cards = vec![
card_val("XTENSION", Value::String(String::from("BINTABLE"))),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(2)),
card_val("NAXIS1", Value::Integer(naxis1 as i64)),
card_val("NAXIS2", Value::Integer(naxis2 as i64)),
card_val("PCOUNT", Value::Integer(0)),
card_val("GCOUNT", Value::Integer(1)),
card_val("TFIELDS", Value::Integer(tfields as i64)),
];
for (i, tform) in tforms.iter().enumerate() {
let kw = alloc::format!("TFORM{}", i + 1);
cards.push(card_val(&kw, Value::String(String::from(*tform))));
}
for (i, name) in ttype_names.iter().enumerate() {
if let Some(n) = name {
let kw = alloc::format!("TTYPE{}", i + 1);
cards.push(card_val(&kw, Value::String(String::from(*n))));
}
}
cards
}
fn parse_test_hdu(fits_data: &[u8]) -> (Vec<u8>, Hdu) {
use crate::hdu::parse_fits;
let primary_cards = vec![
card_val("SIMPLE", Value::Logical(true)),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(0)),
];
let primary_header = serialize_header(&primary_cards).unwrap();
let mut full = Vec::new();
full.extend_from_slice(&primary_header);
full.extend_from_slice(fits_data);
let fits = parse_fits(&full).unwrap();
let hdu = fits.hdus.into_iter().nth(1).unwrap();
(full, hdu)
}
#[test]
fn parse_columns_basic() {
let cards = make_bintable_header(12, 10, 2, &["1J", "1D"], &[Some("X"), Some("Y")]);
let columns = parse_binary_table_columns(&cards, 2).unwrap();
assert_eq!(columns.len(), 2);
assert_eq!(columns[0].name, Some(String::from("X")));
assert_eq!(columns[0].repeat, 1);
assert_eq!(columns[0].col_type, BinaryColumnType::Int);
assert_eq!(columns[0].byte_width, 4);
assert_eq!(columns[1].name, Some(String::from("Y")));
assert_eq!(columns[1].repeat, 1);
assert_eq!(columns[1].col_type, BinaryColumnType::Double);
assert_eq!(columns[1].byte_width, 8);
}
#[test]
fn parse_columns_no_names() {
let cards = make_bintable_header(8, 5, 2, &["1J", "1E"], &[None, None]);
let columns = parse_binary_table_columns(&cards, 2).unwrap();
assert!(columns[0].name.is_none());
assert!(columns[1].name.is_none());
}
#[test]
fn read_int_column() {
let naxis1 = 4;
let naxis2 = 3;
let header = make_bintable_header(naxis1, naxis2, 1, &["1J"], &[Some("VAL")]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_i32_be(&mut raw_data[0..], 100);
write_i32_be(&mut raw_data[4..], 200);
write_i32_be(&mut raw_data[8..], -300);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let col = read_binary_column(&full_fits, &hdu, 0).unwrap();
match col {
BinaryColumnData::Int(vals) => {
assert_eq!(vals, vec![100, 200, -300]);
}
other => panic!("Expected Int, got {:?}", other),
}
}
#[test]
fn read_float_column() {
let naxis1 = 4;
let naxis2 = 2;
let header = make_bintable_header(naxis1, naxis2, 1, &["1E"], &[None]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_f32_be(&mut raw_data[0..], 1.5);
write_f32_be(&mut raw_data[4..], -2.5);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let col = read_binary_column(&full_fits, &hdu, 0).unwrap();
match col {
BinaryColumnData::Float(vals) => {
assert_eq!(vals.len(), 2);
assert!((vals[0] - 1.5).abs() < 1e-6);
assert!((vals[1] - (-2.5)).abs() < 1e-6);
}
other => panic!("Expected Float, got {:?}", other),
}
}
#[test]
fn read_double_column() {
let naxis1 = 8;
let naxis2 = 2;
let header = make_bintable_header(naxis1, naxis2, 1, &["1D"], &[None]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_f64_be(&mut raw_data[0..], 3.125);
write_f64_be(&mut raw_data[8..], -2.625);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let col = read_binary_column(&full_fits, &hdu, 0).unwrap();
match col {
BinaryColumnData::Double(vals) => {
assert_eq!(vals.len(), 2);
assert!((vals[0] - 3.125).abs() < 1e-10);
assert!((vals[1] - (-2.625)).abs() < 1e-10);
}
other => panic!("Expected Double, got {:?}", other),
}
}
#[test]
fn read_short_column() {
let naxis1 = 2;
let naxis2 = 2;
let header = make_bintable_header(naxis1, naxis2, 1, &["1I"], &[None]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_i16_be(&mut raw_data[0..], 1000);
write_i16_be(&mut raw_data[2..], -2000);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let col = read_binary_column(&full_fits, &hdu, 0).unwrap();
match col {
BinaryColumnData::Short(vals) => {
assert_eq!(vals, vec![1000, -2000]);
}
other => panic!("Expected Short, got {:?}", other),
}
}
#[test]
fn read_long_column() {
let naxis1 = 8;
let naxis2 = 2;
let header = make_bintable_header(naxis1, naxis2, 1, &["1K"], &[None]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_i64_be(&mut raw_data[0..], i64::MAX);
write_i64_be(&mut raw_data[8..], i64::MIN);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let col = read_binary_column(&full_fits, &hdu, 0).unwrap();
match col {
BinaryColumnData::Long(vals) => {
assert_eq!(vals, vec![i64::MAX, i64::MIN]);
}
other => panic!("Expected Long, got {:?}", other),
}
}
#[test]
fn read_logical_column() {
let naxis1 = 1;
let naxis2 = 3;
let header = make_bintable_header(naxis1, naxis2, 1, &["1L"], &[None]);
let raw_data = vec![b'T', b'F', b'T'];
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let col = read_binary_column(&full_fits, &hdu, 0).unwrap();
match col {
BinaryColumnData::Logical(vals) => {
assert_eq!(vals, vec![true, false, true]);
}
other => panic!("Expected Logical, got {:?}", other),
}
}
#[test]
fn read_byte_column() {
let naxis1 = 3;
let naxis2 = 2;
let header = make_bintable_header(naxis1, naxis2, 1, &["3B"], &[None]);
let raw_data = vec![10, 20, 30, 40, 50, 60];
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let col = read_binary_column(&full_fits, &hdu, 0).unwrap();
match col {
BinaryColumnData::Byte(vals) => {
assert_eq!(vals, vec![10, 20, 30, 40, 50, 60]);
}
other => panic!("Expected Byte, got {:?}", other),
}
}
#[test]
fn read_ascii_column() {
let naxis1 = 8;
let naxis2 = 2;
let header = make_bintable_header(naxis1, naxis2, 1, &["8A"], &[Some("NAME")]);
let mut raw_data = vec![b' '; naxis1 * naxis2];
raw_data[..5].copy_from_slice(b"Hello");
raw_data[8..13].copy_from_slice(b"World");
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let col = read_binary_column(&full_fits, &hdu, 0).unwrap();
match col {
BinaryColumnData::Ascii(vals) => {
assert_eq!(vals[0], "Hello");
assert_eq!(vals[1], "World");
}
other => panic!("Expected Ascii, got {:?}", other),
}
}
#[test]
fn read_bit_column() {
let naxis1 = 2;
let naxis2 = 2;
let header = make_bintable_header(naxis1, naxis2, 1, &["16X"], &[None]);
let raw_data = vec![0xFF, 0x00, 0xAA, 0x55];
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let col = read_binary_column(&full_fits, &hdu, 0).unwrap();
match col {
BinaryColumnData::Bit(vals) => {
assert_eq!(vals.len(), 2);
assert_eq!(vals[0], vec![0xFF, 0x00]);
assert_eq!(vals[1], vec![0xAA, 0x55]);
}
other => panic!("Expected Bit, got {:?}", other),
}
}
#[test]
fn read_complex_float_column() {
let naxis1 = 8;
let naxis2 = 2;
let header = make_bintable_header(naxis1, naxis2, 1, &["1C"], &[None]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_f32_be(&mut raw_data[0..], 1.0);
write_f32_be(&mut raw_data[4..], 2.0);
write_f32_be(&mut raw_data[8..], -3.0);
write_f32_be(&mut raw_data[12..], 4.0);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let col = read_binary_column(&full_fits, &hdu, 0).unwrap();
match col {
BinaryColumnData::ComplexFloat(vals) => {
assert_eq!(vals.len(), 2);
assert!((vals[0].0 - 1.0).abs() < 1e-6);
assert!((vals[0].1 - 2.0).abs() < 1e-6);
assert!((vals[1].0 - (-3.0)).abs() < 1e-6);
assert!((vals[1].1 - 4.0).abs() < 1e-6);
}
other => panic!("Expected ComplexFloat, got {:?}", other),
}
}
#[test]
fn read_complex_double_column() {
let naxis1 = 16;
let naxis2 = 1;
let header = make_bintable_header(naxis1, naxis2, 1, &["1M"], &[None]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_f64_be(&mut raw_data[0..], 1.5);
write_f64_be(&mut raw_data[8..], -2.5);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let col = read_binary_column(&full_fits, &hdu, 0).unwrap();
match col {
BinaryColumnData::ComplexDouble(vals) => {
assert_eq!(vals.len(), 1);
assert!((vals[0].0 - 1.5).abs() < 1e-10);
assert!((vals[0].1 - (-2.5)).abs() < 1e-10);
}
other => panic!("Expected ComplexDouble, got {:?}", other),
}
}
#[test]
fn read_multi_column_table() {
let naxis1 = 8;
let naxis2 = 2;
let header =
make_bintable_header(naxis1, naxis2, 2, &["1J", "1E"], &[Some("ID"), Some("VAL")]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_i32_be(&mut raw_data[0..], 42);
write_f32_be(&mut raw_data[4..], 1.5);
write_i32_be(&mut raw_data[8..], 99);
write_f32_be(&mut raw_data[12..], -3.0);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let col0 = read_binary_column(&full_fits, &hdu, 0).unwrap();
match col0 {
BinaryColumnData::Int(vals) => assert_eq!(vals, vec![42, 99]),
other => panic!("Expected Int, got {:?}", other),
}
let col1 = read_binary_column(&full_fits, &hdu, 1).unwrap();
match col1 {
BinaryColumnData::Float(vals) => {
assert!((vals[0] - 1.5).abs() < 1e-6);
assert!((vals[1] - (-3.0)).abs() < 1e-6);
}
other => panic!("Expected Float, got {:?}", other),
}
}
#[test]
fn read_repeat_count_floats() {
let naxis1 = 12;
let naxis2 = 2;
let header = make_bintable_header(naxis1, naxis2, 1, &["3E"], &[None]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_f32_be(&mut raw_data[0..], 1.0);
write_f32_be(&mut raw_data[4..], 2.0);
write_f32_be(&mut raw_data[8..], 3.0);
write_f32_be(&mut raw_data[12..], 4.0);
write_f32_be(&mut raw_data[16..], 5.0);
write_f32_be(&mut raw_data[20..], 6.0);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let col = read_binary_column(&full_fits, &hdu, 0).unwrap();
match col {
BinaryColumnData::Float(vals) => {
assert_eq!(vals.len(), 6);
assert!((vals[0] - 1.0).abs() < 1e-6);
assert!((vals[1] - 2.0).abs() < 1e-6);
assert!((vals[2] - 3.0).abs() < 1e-6);
assert!((vals[3] - 4.0).abs() < 1e-6);
assert!((vals[4] - 5.0).abs() < 1e-6);
assert!((vals[5] - 6.0).abs() < 1e-6);
}
other => panic!("Expected Float, got {:?}", other),
}
}
#[test]
fn read_row_basic() {
let naxis1 = 12;
let naxis2 = 2;
let header =
make_bintable_header(naxis1, naxis2, 2, &["1J", "1D"], &[Some("ID"), Some("VAL")]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_i32_be(&mut raw_data[0..], 10);
write_f64_be(&mut raw_data[4..], 1.5);
write_i32_be(&mut raw_data[12..], 20);
write_f64_be(&mut raw_data[16..], -2.5);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let row0 = read_binary_row(&full_fits, &hdu, 0).unwrap();
assert_eq!(row0.len(), 2);
match &row0[0] {
BinaryColumnData::Int(vals) => assert_eq!(vals, &[10]),
other => panic!("Expected Int, got {:?}", other),
}
match &row0[1] {
BinaryColumnData::Double(vals) => {
assert!((vals[0] - 1.5).abs() < 1e-10);
}
other => panic!("Expected Double, got {:?}", other),
}
let row1 = read_binary_row(&full_fits, &hdu, 1).unwrap();
match &row1[0] {
BinaryColumnData::Int(vals) => assert_eq!(vals, &[20]),
other => panic!("Expected Int, got {:?}", other),
}
match &row1[1] {
BinaryColumnData::Double(vals) => {
assert!((vals[0] - (-2.5)).abs() < 1e-10);
}
other => panic!("Expected Double, got {:?}", other),
}
}
#[test]
fn read_row_out_of_bounds() {
let naxis1 = 4;
let naxis2 = 1;
let header = make_bintable_header(naxis1, naxis2, 1, &["1J"], &[None]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_i32_be(&mut raw_data[0..], 42);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
assert!(read_binary_row(&full_fits, &hdu, 1).is_err());
}
#[test]
fn serialize_int_value() {
let data = BinaryColumnData::Int(vec![42, -99]);
let bytes = serialize_binary_column_value(&BinaryColumnType::Int, 1, &data, 0).unwrap();
assert_eq!(bytes.len(), 4);
assert_eq!(read_i32_be(&bytes), 42);
let bytes = serialize_binary_column_value(&BinaryColumnType::Int, 1, &data, 1).unwrap();
assert_eq!(read_i32_be(&bytes), -99);
}
#[test]
fn serialize_float_value() {
let data = BinaryColumnData::Float(vec![1.5, -2.5]);
let bytes = serialize_binary_column_value(&BinaryColumnType::Float, 1, &data, 0).unwrap();
assert_eq!(bytes.len(), 4);
assert!((read_f32_be(&bytes) - 1.5).abs() < 1e-6);
}
#[test]
fn serialize_double_value() {
let data = BinaryColumnData::Double(vec![3.125]);
let bytes = serialize_binary_column_value(&BinaryColumnType::Double, 1, &data, 0).unwrap();
assert_eq!(bytes.len(), 8);
assert!((read_f64_be(&bytes) - 3.125).abs() < 1e-10);
}
#[test]
fn serialize_logical_value() {
let data = BinaryColumnData::Logical(vec![true, false]);
let bytes = serialize_binary_column_value(&BinaryColumnType::Logical, 1, &data, 0).unwrap();
assert_eq!(bytes, vec![b'T']);
let bytes = serialize_binary_column_value(&BinaryColumnType::Logical, 1, &data, 1).unwrap();
assert_eq!(bytes, vec![b'F']);
}
#[test]
fn serialize_ascii_value() {
let data = BinaryColumnData::Ascii(vec![String::from("Hi")]);
let bytes = serialize_binary_column_value(&BinaryColumnType::Ascii, 5, &data, 0).unwrap();
assert_eq!(bytes.len(), 5);
assert_eq!(&bytes[..2], b"Hi");
assert_eq!(&bytes[2..], b" ");
}
#[test]
fn serialize_type_mismatch() {
let data = BinaryColumnData::Int(vec![42]);
let result = serialize_binary_column_value(&BinaryColumnType::Float, 1, &data, 0);
assert!(result.is_err());
}
#[test]
fn build_cards_basic() {
let columns = vec![
BinaryColumnDescriptor {
name: Some(String::from("ID")),
repeat: 1,
col_type: BinaryColumnType::Int,
byte_width: 4,
tdim: None,
},
BinaryColumnDescriptor {
name: Some(String::from("VAL")),
repeat: 1,
col_type: BinaryColumnType::Double,
byte_width: 8,
tdim: None,
},
];
let cards = build_binary_table_cards(&columns, 100, 0).unwrap();
let xtension = cards
.iter()
.find(|c| c.keyword_str() == "XTENSION")
.unwrap();
assert_eq!(
xtension.value,
Some(Value::String(String::from("BINTABLE")))
);
let naxis1 = cards.iter().find(|c| c.keyword_str() == "NAXIS1").unwrap();
assert_eq!(naxis1.value, Some(Value::Integer(12)));
let naxis2 = cards.iter().find(|c| c.keyword_str() == "NAXIS2").unwrap();
assert_eq!(naxis2.value, Some(Value::Integer(100)));
let tfields = cards.iter().find(|c| c.keyword_str() == "TFIELDS").unwrap();
assert_eq!(tfields.value, Some(Value::Integer(2)));
let tform1 = cards.iter().find(|c| c.keyword_str() == "TFORM1").unwrap();
assert_eq!(tform1.value, Some(Value::String(String::from("1J"))));
let ttype1 = cards.iter().find(|c| c.keyword_str() == "TTYPE1").unwrap();
assert_eq!(ttype1.value, Some(Value::String(String::from("ID"))));
let tform2 = cards.iter().find(|c| c.keyword_str() == "TFORM2").unwrap();
assert_eq!(tform2.value, Some(Value::String(String::from("1D"))));
}
#[test]
fn serialize_table_padded() {
let columns = vec![BinaryColumnDescriptor {
name: None,
repeat: 1,
col_type: BinaryColumnType::Int,
byte_width: 4,
tdim: None,
}];
let col_data = vec![BinaryColumnData::Int(vec![1, 2, 3])];
let buf = serialize_binary_table(&columns, &col_data, 3).unwrap();
assert_eq!(buf.len() % BLOCK_SIZE, 0);
assert_eq!(buf.len(), BLOCK_SIZE);
assert_eq!(read_i32_be(&buf[0..]), 1);
assert_eq!(read_i32_be(&buf[4..]), 2);
assert_eq!(read_i32_be(&buf[8..]), 3);
for &b in &buf[12..] {
assert_eq!(b, 0);
}
}
#[test]
fn serialize_table_mismatched_columns() {
let columns = vec![BinaryColumnDescriptor {
name: None,
repeat: 1,
col_type: BinaryColumnType::Int,
byte_width: 4,
tdim: None,
}];
let col_data: Vec<BinaryColumnData> = vec![];
assert!(serialize_binary_table(&columns, &col_data, 1).is_err());
}
#[test]
fn roundtrip_int_column() {
let columns = vec![BinaryColumnDescriptor {
name: Some(String::from("X")),
repeat: 1,
col_type: BinaryColumnType::Int,
byte_width: 4,
tdim: None,
}];
let original = vec![BinaryColumnData::Int(vec![10, 20, 30])];
let naxis2 = 3;
let data_bytes = serialize_binary_table(&columns, &original, naxis2).unwrap();
let cards = build_binary_table_cards(&columns, naxis2, 0).unwrap();
let header_bytes = serialize_header(&cards).unwrap();
let mut fits = Vec::new();
let primary_cards = vec![
card_val("SIMPLE", Value::Logical(true)),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(0)),
];
let primary_header = serialize_header(&primary_cards).unwrap();
fits.extend_from_slice(&primary_header);
fits.extend_from_slice(&header_bytes);
fits.extend_from_slice(&data_bytes);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column(&fits, hdu, 0).unwrap();
assert_eq!(col, BinaryColumnData::Int(vec![10, 20, 30]));
}
#[test]
fn roundtrip_float_column() {
let columns = vec![BinaryColumnDescriptor {
name: None,
repeat: 1,
col_type: BinaryColumnType::Float,
byte_width: 4,
tdim: None,
}];
let original = vec![BinaryColumnData::Float(vec![1.5, -2.5, 0.0])];
let naxis2 = 3;
let data_bytes = serialize_binary_table(&columns, &original, naxis2).unwrap();
let cards = build_binary_table_cards(&columns, naxis2, 0).unwrap();
let header_bytes = serialize_header(&cards).unwrap();
let mut fits = Vec::new();
let primary_cards = vec![
card_val("SIMPLE", Value::Logical(true)),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(0)),
];
fits.extend_from_slice(&serialize_header(&primary_cards).unwrap());
fits.extend_from_slice(&header_bytes);
fits.extend_from_slice(&data_bytes);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column(&fits, hdu, 0).unwrap();
assert_eq!(col, BinaryColumnData::Float(vec![1.5, -2.5, 0.0]));
}
#[test]
fn roundtrip_double_column() {
let columns = vec![BinaryColumnDescriptor {
name: None,
repeat: 1,
col_type: BinaryColumnType::Double,
byte_width: 8,
tdim: None,
}];
let original = vec![BinaryColumnData::Double(vec![3.125, -2.625])];
let naxis2 = 2;
let data_bytes = serialize_binary_table(&columns, &original, naxis2).unwrap();
let cards = build_binary_table_cards(&columns, naxis2, 0).unwrap();
let header_bytes = serialize_header(&cards).unwrap();
let mut fits = Vec::new();
let primary_cards = vec![
card_val("SIMPLE", Value::Logical(true)),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(0)),
];
fits.extend_from_slice(&serialize_header(&primary_cards).unwrap());
fits.extend_from_slice(&header_bytes);
fits.extend_from_slice(&data_bytes);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column(&fits, hdu, 0).unwrap();
match col {
BinaryColumnData::Double(vals) => {
assert!((vals[0] - 3.125).abs() < 1e-10);
assert!((vals[1] - (-2.625)).abs() < 1e-10);
}
other => panic!("Expected Double, got {:?}", other),
}
}
#[test]
fn roundtrip_multi_column() {
let columns = vec![
BinaryColumnDescriptor {
name: Some(String::from("ID")),
repeat: 1,
col_type: BinaryColumnType::Int,
byte_width: 4,
tdim: None,
},
BinaryColumnDescriptor {
name: Some(String::from("NAME")),
repeat: 10,
col_type: BinaryColumnType::Ascii,
byte_width: 10,
tdim: None,
},
BinaryColumnDescriptor {
name: Some(String::from("VALUE")),
repeat: 1,
col_type: BinaryColumnType::Double,
byte_width: 8,
tdim: None,
},
];
let col_data = vec![
BinaryColumnData::Int(vec![1, 2]),
BinaryColumnData::Ascii(vec![String::from("alpha"), String::from("beta")]),
BinaryColumnData::Double(vec![1.5, 2.5]),
];
let naxis2 = 2;
let data_bytes = serialize_binary_table(&columns, &col_data, naxis2).unwrap();
let cards = build_binary_table_cards(&columns, naxis2, 0).unwrap();
let header_bytes = serialize_header(&cards).unwrap();
let mut fits = Vec::new();
let primary_cards = vec![
card_val("SIMPLE", Value::Logical(true)),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(0)),
];
fits.extend_from_slice(&serialize_header(&primary_cards).unwrap());
fits.extend_from_slice(&header_bytes);
fits.extend_from_slice(&data_bytes);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let id_col = read_binary_column(&fits, hdu, 0).unwrap();
assert_eq!(id_col, BinaryColumnData::Int(vec![1, 2]));
let name_col = read_binary_column(&fits, hdu, 1).unwrap();
match name_col {
BinaryColumnData::Ascii(vals) => {
assert_eq!(vals[0], "alpha");
assert_eq!(vals[1], "beta");
}
other => panic!("Expected Ascii, got {:?}", other),
}
let val_col = read_binary_column(&fits, hdu, 2).unwrap();
match val_col {
BinaryColumnData::Double(vals) => {
assert!((vals[0] - 1.5).abs() < 1e-10);
assert!((vals[1] - 2.5).abs() < 1e-10);
}
other => panic!("Expected Double, got {:?}", other),
}
}
#[test]
fn roundtrip_logical_column() {
let columns = vec![BinaryColumnDescriptor {
name: None,
repeat: 1,
col_type: BinaryColumnType::Logical,
byte_width: 1,
tdim: None,
}];
let original = vec![BinaryColumnData::Logical(vec![true, false, true])];
let naxis2 = 3;
let data_bytes = serialize_binary_table(&columns, &original, naxis2).unwrap();
let cards = build_binary_table_cards(&columns, naxis2, 0).unwrap();
let header_bytes = serialize_header(&cards).unwrap();
let mut fits = Vec::new();
let primary_cards = vec![
card_val("SIMPLE", Value::Logical(true)),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(0)),
];
fits.extend_from_slice(&serialize_header(&primary_cards).unwrap());
fits.extend_from_slice(&header_bytes);
fits.extend_from_slice(&data_bytes);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column(&fits, hdu, 0).unwrap();
assert_eq!(col, BinaryColumnData::Logical(vec![true, false, true]));
}
#[test]
fn roundtrip_short_column() {
let columns = vec![BinaryColumnDescriptor {
name: None,
repeat: 1,
col_type: BinaryColumnType::Short,
byte_width: 2,
tdim: None,
}];
let original = vec![BinaryColumnData::Short(vec![100, -200])];
let naxis2 = 2;
let data_bytes = serialize_binary_table(&columns, &original, naxis2).unwrap();
let cards = build_binary_table_cards(&columns, naxis2, 0).unwrap();
let header_bytes = serialize_header(&cards).unwrap();
let mut fits = Vec::new();
let primary_cards = vec![
card_val("SIMPLE", Value::Logical(true)),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(0)),
];
fits.extend_from_slice(&serialize_header(&primary_cards).unwrap());
fits.extend_from_slice(&header_bytes);
fits.extend_from_slice(&data_bytes);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column(&fits, hdu, 0).unwrap();
assert_eq!(col, BinaryColumnData::Short(vec![100, -200]));
}
#[test]
fn roundtrip_long_column() {
let columns = vec![BinaryColumnDescriptor {
name: None,
repeat: 1,
col_type: BinaryColumnType::Long,
byte_width: 8,
tdim: None,
}];
let original = vec![BinaryColumnData::Long(vec![i64::MAX, i64::MIN])];
let naxis2 = 2;
let data_bytes = serialize_binary_table(&columns, &original, naxis2).unwrap();
let cards = build_binary_table_cards(&columns, naxis2, 0).unwrap();
let header_bytes = serialize_header(&cards).unwrap();
let mut fits = Vec::new();
let primary_cards = vec![
card_val("SIMPLE", Value::Logical(true)),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(0)),
];
fits.extend_from_slice(&serialize_header(&primary_cards).unwrap());
fits.extend_from_slice(&header_bytes);
fits.extend_from_slice(&data_bytes);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column(&fits, hdu, 0).unwrap();
assert_eq!(col, BinaryColumnData::Long(vec![i64::MAX, i64::MIN]));
}
#[test]
fn roundtrip_byte_column() {
let columns = vec![BinaryColumnDescriptor {
name: None,
repeat: 3,
col_type: BinaryColumnType::Byte,
byte_width: 3,
tdim: None,
}];
let original = vec![BinaryColumnData::Byte(vec![10, 20, 30, 40, 50, 60])];
let naxis2 = 2;
let data_bytes = serialize_binary_table(&columns, &original, naxis2).unwrap();
let cards = build_binary_table_cards(&columns, naxis2, 0).unwrap();
let header_bytes = serialize_header(&cards).unwrap();
let mut fits = Vec::new();
let primary_cards = vec![
card_val("SIMPLE", Value::Logical(true)),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(0)),
];
fits.extend_from_slice(&serialize_header(&primary_cards).unwrap());
fits.extend_from_slice(&header_bytes);
fits.extend_from_slice(&data_bytes);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column(&fits, hdu, 0).unwrap();
assert_eq!(col, BinaryColumnData::Byte(vec![10, 20, 30, 40, 50, 60]));
}
#[test]
fn roundtrip_complex_float_column() {
let columns = vec![BinaryColumnDescriptor {
name: None,
repeat: 1,
col_type: BinaryColumnType::ComplexFloat,
byte_width: 8,
tdim: None,
}];
let original = vec![BinaryColumnData::ComplexFloat(vec![
(1.0, 2.0),
(-3.0, 4.0),
])];
let naxis2 = 2;
let data_bytes = serialize_binary_table(&columns, &original, naxis2).unwrap();
let cards = build_binary_table_cards(&columns, naxis2, 0).unwrap();
let header_bytes = serialize_header(&cards).unwrap();
let mut fits = Vec::new();
let primary_cards = vec![
card_val("SIMPLE", Value::Logical(true)),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(0)),
];
fits.extend_from_slice(&serialize_header(&primary_cards).unwrap());
fits.extend_from_slice(&header_bytes);
fits.extend_from_slice(&data_bytes);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column(&fits, hdu, 0).unwrap();
assert_eq!(
col,
BinaryColumnData::ComplexFloat(vec![(1.0, 2.0), (-3.0, 4.0)])
);
}
#[test]
fn roundtrip_complex_double_column() {
let columns = vec![BinaryColumnDescriptor {
name: None,
repeat: 1,
col_type: BinaryColumnType::ComplexDouble,
byte_width: 16,
tdim: None,
}];
let original = vec![BinaryColumnData::ComplexDouble(vec![(1.5, -2.5)])];
let naxis2 = 1;
let data_bytes = serialize_binary_table(&columns, &original, naxis2).unwrap();
let cards = build_binary_table_cards(&columns, naxis2, 0).unwrap();
let header_bytes = serialize_header(&cards).unwrap();
let mut fits = Vec::new();
let primary_cards = vec![
card_val("SIMPLE", Value::Logical(true)),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(0)),
];
fits.extend_from_slice(&serialize_header(&primary_cards).unwrap());
fits.extend_from_slice(&header_bytes);
fits.extend_from_slice(&data_bytes);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column(&fits, hdu, 0).unwrap();
assert_eq!(col, BinaryColumnData::ComplexDouble(vec![(1.5, -2.5)]));
}
#[test]
fn col_index_out_of_bounds() {
let naxis1 = 4;
let naxis2 = 1;
let header = make_bintable_header(naxis1, naxis2, 1, &["1J"], &[None]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_i32_be(&mut raw_data[0..], 42);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
assert!(read_binary_column(&full_fits, &hdu, 1).is_err());
}
#[test]
fn serialize_binary_table_hdu_produces_valid_fits() {
let columns = vec![
BinaryColumnDescriptor {
name: Some(String::from("X")),
repeat: 1,
col_type: BinaryColumnType::Int,
byte_width: 4,
tdim: None,
},
BinaryColumnDescriptor {
name: Some(String::from("Y")),
repeat: 1,
col_type: BinaryColumnType::Double,
byte_width: 8,
tdim: None,
},
];
let col_data = vec![
BinaryColumnData::Int(vec![1, 2]),
BinaryColumnData::Double(vec![1.5, 2.5]),
];
let hdu_bytes = serialize_binary_table_hdu(&columns, &col_data, 2).unwrap();
assert_eq!(hdu_bytes.len() % BLOCK_SIZE, 0);
let mut fits = Vec::new();
let primary_cards = vec![
card_val("SIMPLE", Value::Logical(true)),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(0)),
];
fits.extend_from_slice(&serialize_header(&primary_cards).unwrap());
fits.extend_from_slice(&hdu_bytes);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
assert_eq!(parsed.len(), 2);
let hdu = parsed.get(1).unwrap();
let x_col = read_binary_column(&fits, hdu, 0).unwrap();
assert_eq!(x_col, BinaryColumnData::Int(vec![1, 2]));
let y_col = read_binary_column(&fits, hdu, 1).unwrap();
match y_col {
BinaryColumnData::Double(vals) => {
assert!((vals[0] - 1.5).abs() < 1e-10);
assert!((vals[1] - 2.5).abs() < 1e-10);
}
other => panic!("Expected Double, got {:?}", other),
}
}
#[test]
fn parse_tdim_2d() {
assert_eq!(parse_tdim("(10,20)"), Some(vec![10, 20]));
}
#[test]
fn parse_tdim_3d() {
assert_eq!(parse_tdim("(3,4,5)"), Some(vec![3, 4, 5]));
}
#[test]
fn parse_tdim_1d() {
assert_eq!(parse_tdim("(100)"), Some(vec![100]));
}
#[test]
fn parse_tdim_whitespace() {
assert_eq!(parse_tdim(" ( 10 , 20 ) "), Some(vec![10, 20]));
}
#[test]
fn parse_tdim_empty() {
assert_eq!(parse_tdim(""), None);
}
#[test]
fn parse_tdim_no_parens() {
assert_eq!(parse_tdim("10,20"), None);
}
#[test]
fn parse_tdim_bad_value() {
assert_eq!(parse_tdim("(10,abc)"), None);
}
#[test]
fn tdim_roundtrip_via_header() {
let columns = vec![BinaryColumnDescriptor {
name: Some(String::from("IMG")),
repeat: 200,
col_type: BinaryColumnType::Float,
byte_width: 800,
tdim: Some(vec![10, 20]),
}];
let cards = build_binary_table_cards(&columns, 1, 0).unwrap();
let tdim_card = cards.iter().find(|c| c.keyword_str() == "TDIM1").unwrap();
assert_eq!(
tdim_card.value,
Some(Value::String(String::from("(10,20)")))
);
let parsed_cols = parse_binary_table_columns(&cards, 1).unwrap();
assert_eq!(parsed_cols[0].tdim, Some(vec![10, 20]));
}
#[test]
fn tdim_absent_yields_none() {
let cards = make_bintable_header(4, 1, 1, &["1J"], &[Some("X")]);
let parsed_cols = parse_binary_table_columns(&cards, 1).unwrap();
assert_eq!(parsed_cols[0].tdim, None);
}
#[test]
fn extract_scaling_defaults() {
let cards = make_bintable_header(4, 1, 1, &["1J"], &[Some("X")]);
let (tscal, tzero) = extract_column_scaling(&cards, 1);
assert_eq!(tscal, 1.0);
assert_eq!(tzero, 0.0);
}
#[test]
fn extract_scaling_present() {
let mut cards = make_bintable_header(4, 1, 1, &["1J"], &[Some("X")]);
cards.push(card_val("TSCAL1", Value::Float(2.5)));
cards.push(card_val("TZERO1", Value::Float(100.0)));
let (tscal, tzero) = extract_column_scaling(&cards, 1);
assert_eq!(tscal, 2.5);
assert_eq!(tzero, 100.0);
}
#[test]
fn extract_scaling_integer_keywords() {
let mut cards = make_bintable_header(4, 1, 1, &["1J"], &[Some("X")]);
cards.push(card_val("TSCAL1", Value::Integer(3)));
cards.push(card_val("TZERO1", Value::Integer(32768)));
let (tscal, tzero) = extract_column_scaling(&cards, 1);
assert_eq!(tscal, 3.0);
assert_eq!(tzero, 32768.0);
}
#[test]
fn apply_scaling_identity() {
let data = BinaryColumnData::Int(vec![10, 20, 30]);
let result = apply_column_scaling(&data, 1.0, 0.0);
assert_eq!(result, vec![10.0, 20.0, 30.0]);
}
#[test]
fn apply_scaling_with_offset() {
let data = BinaryColumnData::Short(vec![0, 1, 2]);
let result = apply_column_scaling(&data, 2.0, 100.0);
assert_eq!(result, vec![100.0, 102.0, 104.0]);
}
#[test]
fn apply_scaling_unsigned_short() {
let data = BinaryColumnData::Short(vec![0, -32768, 32767]);
let result = apply_column_scaling(&data, 1.0, 32768.0);
assert_eq!(result, vec![32768.0, 0.0, 65535.0]);
}
#[test]
fn read_physical_column_with_scaling() {
let naxis1 = 4;
let naxis2 = 3;
let mut header = make_bintable_header(naxis1, naxis2, 1, &["1J"], &[Some("VAL")]);
header.push(card_val("TSCAL1", Value::Float(2.0)));
header.push(card_val("TZERO1", Value::Float(10.0)));
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_i32_be(&mut raw_data[0..], 1);
write_i32_be(&mut raw_data[4..], 2);
write_i32_be(&mut raw_data[8..], 3);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let physical = read_binary_column_physical(&full_fits, &hdu, 0).unwrap();
assert_eq!(physical, vec![12.0, 14.0, 16.0]);
}
#[test]
fn read_physical_column_no_scaling() {
let naxis1 = 4;
let naxis2 = 2;
let header = make_bintable_header(naxis1, naxis2, 1, &["1J"], &[None]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_i32_be(&mut raw_data[0..], 100);
write_i32_be(&mut raw_data[4..], 200);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let physical = read_binary_column_physical(&full_fits, &hdu, 0).unwrap();
assert_eq!(physical, vec![100.0, 200.0]);
}
#[test]
fn parse_tform_vararray_p_int() {
let (repeat, col_type) = parse_tform_binary("1PJ").unwrap();
assert_eq!(repeat, 1);
assert_eq!(col_type, BinaryColumnType::VarArrayP('J'));
}
#[test]
fn parse_tform_vararray_q_double() {
let (repeat, col_type) = parse_tform_binary("1QD").unwrap();
assert_eq!(repeat, 1);
assert_eq!(col_type, BinaryColumnType::VarArrayQ('D'));
}
#[test]
fn parse_tform_vararray_p_with_maxlen() {
let (repeat, col_type) = parse_tform_binary("1PB(200)").unwrap();
assert_eq!(repeat, 1);
assert_eq!(col_type, BinaryColumnType::VarArrayP('B'));
}
#[test]
fn parse_tform_vararray_p_float() {
let (repeat, col_type) = parse_tform_binary("1PE(1000)").unwrap();
assert_eq!(repeat, 1);
assert_eq!(col_type, BinaryColumnType::VarArrayP('E'));
}
#[test]
fn parse_tform_vararray_q_with_maxlen() {
let (repeat, col_type) = parse_tform_binary("1QJ(500)").unwrap();
assert_eq!(repeat, 1);
assert_eq!(col_type, BinaryColumnType::VarArrayQ('J'));
}
#[test]
fn parse_tform_vararray_invalid_elem() {
assert!(parse_tform_binary("1PZ").is_err());
}
#[test]
fn tform_string_vararray_p() {
assert_eq!(tform_string(1, &BinaryColumnType::VarArrayP('J')), "1PJ");
}
#[test]
fn tform_string_vararray_q() {
assert_eq!(tform_string(1, &BinaryColumnType::VarArrayQ('E')), "1QE");
}
#[test]
fn byte_size_vararray() {
assert_eq!(binary_type_byte_size(&BinaryColumnType::VarArrayP('J')), 8);
assert_eq!(binary_type_byte_size(&BinaryColumnType::VarArrayQ('D')), 16);
}
#[test]
fn byte_width_vararray() {
assert_eq!(compute_byte_width(1, &BinaryColumnType::VarArrayP('J')), 8);
assert_eq!(compute_byte_width(1, &BinaryColumnType::VarArrayQ('D')), 16);
}
fn build_vla_fits(
naxis2: usize,
tform: &str,
descriptors: &[(i32, i32)], heap: &[u8],
) -> Vec<u8> {
let desc_size = 8; let naxis1 = desc_size;
let pcount = heap.len();
let header_cards = vec![
card_val("XTENSION", Value::String(String::from("BINTABLE"))),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(2)),
card_val("NAXIS1", Value::Integer(naxis1 as i64)),
card_val("NAXIS2", Value::Integer(naxis2 as i64)),
card_val("PCOUNT", Value::Integer(pcount as i64)),
card_val("GCOUNT", Value::Integer(1)),
card_val("TFIELDS", Value::Integer(1)),
card_val("TFORM1", Value::String(String::from(tform))),
];
let header = serialize_header(&header_cards).unwrap();
let main_data_len = naxis1 * naxis2;
let total_data = main_data_len + pcount;
let padded = padded_byte_len(total_data);
let mut data = vec![0u8; padded];
for (i, (count, offset)) in descriptors.iter().enumerate() {
let pos = i * naxis1;
write_i32_be(&mut data[pos..], *count);
write_i32_be(&mut data[pos + 4..], *offset);
}
data[main_data_len..main_data_len + heap.len()].copy_from_slice(heap);
let primary_cards = vec![
card_val("SIMPLE", Value::Logical(true)),
card_val("BITPIX", Value::Integer(8)),
card_val("NAXIS", Value::Integer(0)),
];
let primary_header = serialize_header(&primary_cards).unwrap();
let mut fits = Vec::new();
fits.extend_from_slice(&primary_header);
fits.extend_from_slice(&header);
fits.extend_from_slice(&data);
fits
}
#[test]
fn read_vla_byte_column() {
let heap = vec![10u8, 20, 30, 40, 50];
let descriptors = vec![
(3, 0), (2, 3), ];
let fits = build_vla_fits(2, "1PB", &descriptors, &heap);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column_vla(&fits, hdu, 0).unwrap();
match col {
BinaryColumnData::VarByte(rows) => {
assert_eq!(rows.len(), 2);
assert_eq!(rows[0], vec![10, 20, 30]);
assert_eq!(rows[1], vec![40, 50]);
}
other => panic!("Expected VarByte, got {:?}", other),
}
}
#[test]
fn read_vla_int_column() {
let mut heap = vec![0u8; 12]; write_i32_be(&mut heap[0..], 100);
write_i32_be(&mut heap[4..], 200);
write_i32_be(&mut heap[8..], 300);
let descriptors = vec![
(2, 0), (1, 8), ];
let fits = build_vla_fits(2, "1PJ", &descriptors, &heap);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column_vla(&fits, hdu, 0).unwrap();
match col {
BinaryColumnData::VarInt(rows) => {
assert_eq!(rows.len(), 2);
assert_eq!(rows[0], vec![100, 200]);
assert_eq!(rows[1], vec![300]);
}
other => panic!("Expected VarInt, got {:?}", other),
}
}
#[test]
fn read_vla_float_column() {
let mut heap = vec![0u8; 12]; write_f32_be(&mut heap[0..], 1.5);
write_f32_be(&mut heap[4..], -2.5);
write_f32_be(&mut heap[8..], 3.0);
let descriptors = vec![
(2, 0), (1, 8), ];
let fits = build_vla_fits(2, "1PE", &descriptors, &heap);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column_vla(&fits, hdu, 0).unwrap();
match col {
BinaryColumnData::VarFloat(rows) => {
assert_eq!(rows.len(), 2);
assert!((rows[0][0] - 1.5).abs() < 1e-6);
assert!((rows[0][1] - (-2.5)).abs() < 1e-6);
assert!((rows[1][0] - 3.0).abs() < 1e-6);
}
other => panic!("Expected VarFloat, got {:?}", other),
}
}
#[test]
fn read_vla_double_column() {
let mut heap = vec![0u8; 16]; write_f64_be(&mut heap[0..], 3.125);
write_f64_be(&mut heap[8..], -2.625);
let descriptors = vec![(1, 0), (1, 8)];
let fits = build_vla_fits(2, "1PD", &descriptors, &heap);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column_vla(&fits, hdu, 0).unwrap();
match col {
BinaryColumnData::VarDouble(rows) => {
assert_eq!(rows.len(), 2);
assert!((rows[0][0] - 3.125).abs() < 1e-10);
assert!((rows[1][0] - (-2.625)).abs() < 1e-10);
}
other => panic!("Expected VarDouble, got {:?}", other),
}
}
#[test]
fn read_vla_short_column() {
let mut heap = vec![0u8; 6]; write_i16_be(&mut heap[0..], 1000);
write_i16_be(&mut heap[2..], -2000);
write_i16_be(&mut heap[4..], 3000);
let descriptors = vec![(2, 0), (1, 4)];
let fits = build_vla_fits(2, "1PI", &descriptors, &heap);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column_vla(&fits, hdu, 0).unwrap();
match col {
BinaryColumnData::VarShort(rows) => {
assert_eq!(rows.len(), 2);
assert_eq!(rows[0], vec![1000, -2000]);
assert_eq!(rows[1], vec![3000]);
}
other => panic!("Expected VarShort, got {:?}", other),
}
}
#[test]
fn read_vla_long_column() {
let mut heap = vec![0u8; 16]; write_i64_be(&mut heap[0..], i64::MAX);
write_i64_be(&mut heap[8..], i64::MIN);
let descriptors = vec![(1, 0), (1, 8)];
let fits = build_vla_fits(2, "1PK", &descriptors, &heap);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column_vla(&fits, hdu, 0).unwrap();
match col {
BinaryColumnData::VarLong(rows) => {
assert_eq!(rows.len(), 2);
assert_eq!(rows[0], vec![i64::MAX]);
assert_eq!(rows[1], vec![i64::MIN]);
}
other => panic!("Expected VarLong, got {:?}", other),
}
}
#[test]
fn read_vla_empty_row() {
let heap = vec![10u8, 20, 30];
let descriptors = vec![
(0, 0), (3, 0), ];
let fits = build_vla_fits(2, "1PB", &descriptors, &heap);
let parsed = crate::hdu::parse_fits(&fits).unwrap();
let hdu = parsed.get(1).unwrap();
let col = read_binary_column_vla(&fits, hdu, 0).unwrap();
match col {
BinaryColumnData::VarByte(rows) => {
assert_eq!(rows[0], Vec::<u8>::new());
assert_eq!(rows[1], vec![10, 20, 30]);
}
other => panic!("Expected VarByte, got {:?}", other),
}
}
#[test]
fn read_vla_on_fixed_column_errors() {
let naxis1 = 4;
let naxis2 = 1;
let header = make_bintable_header(naxis1, naxis2, 1, &["1J"], &[None]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
write_i32_be(&mut raw_data[0..], 42);
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
assert!(read_binary_column_vla(&full_fits, &hdu, 0).is_err());
}
#[test]
fn read_column_range_middle() {
let naxis1 = 4;
let naxis2 = 5;
let header = make_bintable_header(naxis1, naxis2, 1, &["1J"], &[Some("X")]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
for i in 0..naxis2 {
write_i32_be(&mut raw_data[i * 4..], (i as i32 + 1) * 10);
}
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
let col = read_binary_column_range(&full_fits, &hdu, 0, 1, 3).unwrap();
assert_eq!(col, BinaryColumnData::Int(vec![20, 30, 40]));
}
#[test]
fn read_column_range_out_of_bounds() {
let naxis1 = 4;
let naxis2 = 3;
let header = make_bintable_header(naxis1, naxis2, 1, &["1J"], &[None]);
let raw_data = vec![0u8; naxis1 * naxis2];
let fits_data = build_bintable_hdu(&header, &raw_data);
let (full_fits, hdu) = parse_test_hdu(&fits_data);
assert!(read_binary_column_range(&full_fits, &hdu, 0, 2, 3).is_err());
}
#[test]
fn write_column_roundtrip() {
let naxis1 = 4;
let naxis2 = 3;
let header = make_bintable_header(naxis1, naxis2, 1, &["1J"], &[Some("X")]);
let mut raw_data = vec![0u8; naxis1 * naxis2];
for i in 0..naxis2 {
write_i32_be(&mut raw_data[i * 4..], 0);
}
let fits_data = build_bintable_hdu(&header, &raw_data);
let (mut full_fits, hdu) = parse_test_hdu(&fits_data);
let new_data = BinaryColumnData::Int(vec![100, 200, 300]);
write_binary_column(&mut full_fits, &hdu, 0, &new_data).unwrap();
let read_back = read_binary_column(&full_fits, &hdu, 0).unwrap();
assert_eq!(read_back, new_data);
}
}