use crate::bin_table::Value;
use std::error::Error;
use std::str::from_utf8;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TableColumnFormat {
String(usize),
StringArray(usize, usize),
Boolean(usize),
Bit(usize),
U8(usize),
I8(usize),
U16(usize),
I16(usize),
U32(usize),
I32(usize),
I64(usize),
F32(usize),
F64(usize),
C32(usize),
M64(usize),
VariableLengthArray {
element: TableElementFormat,
descriptor: ArrayDescriptor,
max: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ArrayDescriptor {
P32,
Q64,
}
impl ArrayDescriptor {
pub fn bytes_len(&self) -> usize {
match self {
ArrayDescriptor::P32 => 8,
ArrayDescriptor::Q64 => 16,
}
}
pub fn read(&self, bytes: &[u8]) -> Option<(usize, usize)> {
let (count, offset) = match self {
ArrayDescriptor::P32 => {
let (count, offset) = bytes.get(..8)?.split_at(4);
(
i32::from_be_bytes(count.try_into().ok()?) as i64,
i32::from_be_bytes(offset.try_into().ok()?) as i64,
)
}
ArrayDescriptor::Q64 => {
let (count, offset) = bytes.get(..16)?.split_at(8);
(
i64::from_be_bytes(count.try_into().ok()?),
i64::from_be_bytes(offset.try_into().ok()?),
)
}
};
Some((usize::try_from(count).ok()?, usize::try_from(offset).ok()?))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TableElementFormat {
Character,
Boolean,
Bit,
U8,
I8,
U16,
I16,
U32,
I32,
I64,
F32,
F64,
C32,
M64,
}
impl TableElementFormat {
pub fn code(&self) -> char {
match self {
TableElementFormat::Character => 'A',
TableElementFormat::Boolean => 'L',
TableElementFormat::Bit => 'X',
TableElementFormat::U8 => 'B',
TableElementFormat::I8 => 'S',
TableElementFormat::U16 => 'U',
TableElementFormat::I16 => 'I',
TableElementFormat::U32 => 'V',
TableElementFormat::I32 => 'J',
TableElementFormat::I64 => 'K',
TableElementFormat::F32 => 'E',
TableElementFormat::F64 => 'D',
TableElementFormat::C32 => 'C',
TableElementFormat::M64 => 'M',
}
}
fn from_code(code: char) -> Option<Self> {
Some(match code {
'A' => TableElementFormat::Character,
'L' => TableElementFormat::Boolean,
'X' => TableElementFormat::Bit,
'B' => TableElementFormat::U8,
'S' => TableElementFormat::I8,
'U' => TableElementFormat::U16,
'I' => TableElementFormat::I16,
'V' => TableElementFormat::U32,
'J' => TableElementFormat::I32,
'K' => TableElementFormat::I64,
'E' => TableElementFormat::F32,
'D' => TableElementFormat::F64,
'C' => TableElementFormat::C32,
'M' => TableElementFormat::M64,
_ => return None,
})
}
pub fn repeated(&self, count: usize) -> TableColumnFormat {
match self {
TableElementFormat::Character => TableColumnFormat::String(count),
TableElementFormat::Boolean => TableColumnFormat::Boolean(count),
TableElementFormat::Bit => TableColumnFormat::Bit(count),
TableElementFormat::U8 => TableColumnFormat::U8(count),
TableElementFormat::I8 => TableColumnFormat::I8(count),
TableElementFormat::U16 => TableColumnFormat::U16(count),
TableElementFormat::I16 => TableColumnFormat::I16(count),
TableElementFormat::U32 => TableColumnFormat::U32(count),
TableElementFormat::I32 => TableColumnFormat::I32(count),
TableElementFormat::I64 => TableColumnFormat::I64(count),
TableElementFormat::F32 => TableColumnFormat::F32(count),
TableElementFormat::F64 => TableColumnFormat::F64(count),
TableElementFormat::C32 => TableColumnFormat::C32(count),
TableElementFormat::M64 => TableColumnFormat::M64(count),
}
}
}
impl TableColumnFormat {
pub fn parse_into_value(&self, data: &[u8], heap: &[u8]) -> crate::Result<Value> {
if let TableColumnFormat::VariableLengthArray {
element,
descriptor,
..
} = self
{
return self.parse_array_from_heap(*element, *descriptor, data, heap);
}
let width = self.bytes_len();
let bytes = data.get(..width).ok_or_else(|| {
crate::Error::DeserializationError(format!(
"Column of format {} needs {} bytes but only {} remain in the row",
String::from(*self),
width,
data.len()
))
})?;
match self {
TableColumnFormat::String(_) => Ok(Value::String(decode_string(bytes)?)),
TableColumnFormat::StringArray(_, substring_width) => {
let substring_width = (*substring_width).max(1);
Ok(Value::StringArray(
bytes
.chunks(substring_width)
.map(decode_string)
.collect::<crate::Result<_>>()?,
))
}
TableColumnFormat::Boolean(_) => Ok(Value::Boolean(
bytes.iter().map(|byte| *byte == b'T').collect(),
)),
TableColumnFormat::Bit(count) => Ok(Value::Bit {
bytes: bytes.to_vec(),
len: *count,
}),
TableColumnFormat::U8(_) => Ok(Value::U8(bytes.to_vec())),
TableColumnFormat::I8(_) => {
Ok(Value::I8(bytes.iter().map(|byte| *byte as i8).collect()))
}
TableColumnFormat::U16(_) => Ok(Value::U16(
bytes
.as_chunks::<2>()
.0
.iter()
.map(|value| u16::from_be_bytes(*value))
.collect(),
)),
TableColumnFormat::I16(_) => Ok(Value::I16(
bytes
.as_chunks::<2>()
.0
.iter()
.map(|value| i16::from_be_bytes(*value))
.collect(),
)),
TableColumnFormat::U32(_) => Ok(Value::U32(
bytes
.as_chunks::<4>()
.0
.iter()
.map(|value| u32::from_be_bytes(*value))
.collect(),
)),
TableColumnFormat::I32(_) => Ok(Value::I32(
bytes
.as_chunks::<4>()
.0
.iter()
.map(|value| i32::from_be_bytes(*value))
.collect(),
)),
TableColumnFormat::I64(_) => Ok(Value::I64(
bytes
.as_chunks::<8>()
.0
.iter()
.map(|value| i64::from_be_bytes(*value))
.collect(),
)),
TableColumnFormat::F32(_) => Ok(Value::F32(
bytes
.as_chunks::<4>()
.0
.iter()
.map(|value| f32::from_be_bytes(*value))
.collect(),
)),
TableColumnFormat::F64(_) => Ok(Value::F64(
bytes
.as_chunks::<8>()
.0
.iter()
.map(|value| f64::from_be_bytes(*value))
.collect(),
)),
TableColumnFormat::C32(_) => Ok(Value::C32(
bytes
.as_chunks::<8>()
.0
.iter()
.map(|value| {
let (real, imaginary) = value.split_at(4);
(
f32::from_be_bytes(real.try_into().expect("4 of 8 bytes")),
f32::from_be_bytes(imaginary.try_into().expect("4 of 8 bytes")),
)
})
.collect(),
)),
TableColumnFormat::M64(_) => Ok(Value::M64(
bytes
.as_chunks::<16>()
.0
.iter()
.map(|value| {
let (real, imaginary) = value.split_at(8);
(
f64::from_be_bytes(real.try_into().expect("8 of 16 bytes")),
f64::from_be_bytes(imaginary.try_into().expect("8 of 16 bytes")),
)
})
.collect(),
)),
TableColumnFormat::VariableLengthArray { .. } => {
unreachable!("a variable length array column is decoded from the heap")
}
}
}
fn parse_array_from_heap(
&self,
element: TableElementFormat,
descriptor: ArrayDescriptor,
data: &[u8],
heap: &[u8],
) -> crate::Result<Value> {
let (count, offset) = descriptor.read(data).ok_or_else(|| {
crate::Error::DeserializationError(format!(
"Column of format {} has an unreadable array descriptor",
String::from(*self)
))
})?;
let format = element.repeated(count);
if count == 0 {
return format.parse_into_value(&[], &[]);
}
let width = format.bytes_len();
let bytes = heap
.get(offset..)
.and_then(|heap| heap.get(..width))
.ok_or_else(|| {
crate::Error::DeserializationError(format!(
"Column of format {} points at bytes {}..{} of a {} byte heap",
String::from(*self),
offset,
offset + width,
heap.len()
))
})?;
format.parse_into_value(bytes, &[])
}
pub fn bytes_len(&self) -> usize {
match self {
TableColumnFormat::String(count) => *count,
TableColumnFormat::StringArray(count, _) => *count,
TableColumnFormat::Bit(count) => count.div_ceil(8),
TableColumnFormat::Boolean(count) => *count,
TableColumnFormat::U8(count) => *count,
TableColumnFormat::I8(count) => *count,
TableColumnFormat::U16(count) => 2 * count,
TableColumnFormat::I16(count) => 2 * count,
TableColumnFormat::U32(count) => 4 * count,
TableColumnFormat::I32(count) => 4 * count,
TableColumnFormat::I64(count) => 8 * count,
TableColumnFormat::F32(count) => 4 * count,
TableColumnFormat::F64(count) => 8 * count,
TableColumnFormat::C32(count) => 8 * count,
TableColumnFormat::M64(count) => 16 * count,
TableColumnFormat::VariableLengthArray { descriptor, .. } => descriptor.bytes_len(),
}
}
pub fn len(&self) -> usize {
match self {
TableColumnFormat::String(_) => 1,
TableColumnFormat::StringArray(count, substring_width) => {
count / (*substring_width).max(1)
}
TableColumnFormat::Boolean(count)
| TableColumnFormat::Bit(count)
| TableColumnFormat::U8(count)
| TableColumnFormat::I8(count)
| TableColumnFormat::U16(count)
| TableColumnFormat::I16(count)
| TableColumnFormat::U32(count)
| TableColumnFormat::I32(count)
| TableColumnFormat::I64(count)
| TableColumnFormat::F32(count)
| TableColumnFormat::F64(count)
| TableColumnFormat::C32(count)
| TableColumnFormat::M64(count) => *count,
TableColumnFormat::VariableLengthArray { max, .. } => *max,
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
fn decode_string(bytes: &[u8]) -> crate::Result<String> {
Ok(from_utf8(bytes)
.map_err(|e| crate::Error::DeserializationError(format!("Not valid UTF-8: {}", e)))?
.replace("\0", "")
.trim_ascii()
.to_string())
}
impl From<TableColumnFormat> for String {
fn from(value: TableColumnFormat) -> String {
match value {
TableColumnFormat::String(repeat) => format!("{}A", repeat),
TableColumnFormat::StringArray(repeat, items) => format!("{}A{}", repeat, items),
TableColumnFormat::Boolean(repeat) => format!("{}L", repeat),
TableColumnFormat::Bit(repeat) => format!("{}X", repeat),
TableColumnFormat::U8(repeat) => format!("{}B", repeat),
TableColumnFormat::I8(repeat) => format!("{}S", repeat),
TableColumnFormat::U16(repeat) => format!("{}U", repeat),
TableColumnFormat::I16(repeat) => format!("{}I", repeat),
TableColumnFormat::U32(repeat) => format!("{}V", repeat),
TableColumnFormat::I32(repeat) => format!("{}J", repeat),
TableColumnFormat::I64(repeat) => format!("{}K", repeat),
TableColumnFormat::F32(repeat) => format!("{}E", repeat),
TableColumnFormat::F64(repeat) => format!("{}D", repeat),
TableColumnFormat::C32(repeat) => format!("{}C", repeat),
TableColumnFormat::M64(repeat) => format!("{}M", repeat),
TableColumnFormat::VariableLengthArray {
element,
descriptor,
max,
} => {
let code = match descriptor {
ArrayDescriptor::P32 => 'P',
ArrayDescriptor::Q64 => 'Q',
};
format!("1{}{}({})", code, element.code(), max)
}
}
}
}
impl TryFrom<String> for TableColumnFormat {
type Error = Box<dyn Error + Send + Sync>;
fn try_from(value: String) -> Result<Self, Self::Error> {
let (repeat, format, items) = extract_parts(&value)?;
match format {
'A' => {
if items > 0 {
Ok(TableColumnFormat::StringArray(repeat, items))
} else {
Ok(TableColumnFormat::String(repeat))
}
}
'L' => Ok(TableColumnFormat::Boolean(repeat)),
'X' => Ok(TableColumnFormat::Bit(repeat)),
'B' => Ok(TableColumnFormat::U8(repeat)),
'S' => Ok(TableColumnFormat::I8(repeat)),
'I' => Ok(TableColumnFormat::I16(repeat)),
'U' => Ok(TableColumnFormat::U16(repeat)),
'J' => Ok(TableColumnFormat::I32(repeat)),
'V' => Ok(TableColumnFormat::U32(repeat)),
'K' => Ok(TableColumnFormat::I64(repeat)),
'E' => Ok(TableColumnFormat::F32(repeat)),
'D' => Ok(TableColumnFormat::F64(repeat)),
'C' => Ok(TableColumnFormat::C32(repeat)),
'M' => Ok(TableColumnFormat::M64(repeat)),
'P' | 'Q' => parse_variable_length_array(&value, repeat, format),
_ => Err(From::from(format!(
"Invalid TableColumnFormat value: {}",
value
))),
}
}
}
fn parse_variable_length_array(
value: &str,
repeat: usize,
code: char,
) -> Result<TableColumnFormat, Box<dyn Error + Send + Sync>> {
if repeat > 1 {
return Err(From::from(format!(
"A variable length array column holds one descriptor, so its repeat count must be 0 \
or 1, but {} says {}",
value, repeat
)));
}
let descriptor = match code {
'P' => ArrayDescriptor::P32,
_ => ArrayDescriptor::Q64,
};
let rest = value
.trim_start_matches(|c: char| c.is_ascii_digit())
.get(1..)
.unwrap_or_default();
let (element_code, rest) = {
let mut chars = rest.chars();
let element_code = chars.next().ok_or_else(|| {
format!(
"Variable length array format {} names no element type",
value
)
})?;
(element_code, chars.as_str())
};
let element = TableElementFormat::from_code(element_code).ok_or_else(|| {
format!(
"Variable length array format {} has an invalid element type: {}",
value, element_code
)
})?;
let max = match rest
.trim()
.strip_prefix('(')
.and_then(|rest| rest.strip_suffix(')'))
{
Some(max) => max.trim().parse::<usize>().map_err(|_| {
format!(
"Variable length array format {} has an invalid maximum",
value
)
})?,
None if rest.trim().is_empty() => 0,
None => {
return Err(From::from(format!(
"Trailing characters in variable length array format {}",
value
)));
}
};
Ok(TableColumnFormat::VariableLengthArray {
element,
descriptor,
max,
})
}
fn extract_parts(value: &str) -> Result<(usize, char, usize), Box<dyn Error + Send + Sync>> {
let mut chars = value.chars().peekable();
let mut repeat_str = String::new();
while let Some(c) = chars.peek() {
if c.is_ascii_digit() {
repeat_str.push(*c);
chars.next();
} else {
break;
}
}
let repeat = if repeat_str.is_empty() {
1
} else {
repeat_str
.parse::<usize>()
.map_err(|_| "Invalid repeat count")?
};
let code = chars
.next()
.ok_or_else(|| "Missing format code".to_string())?;
let mut width_str = String::new();
while let Some(c) = chars.peek() {
if c.is_ascii_digit() {
width_str.push(*c);
chars.next();
} else {
break;
}
}
let width = if width_str.is_empty() {
0
} else {
width_str
.parse::<usize>()
.map_err(|_| "Invalid string width")?
};
Ok((repeat, code, width))
}
#[cfg(test)]
mod tests {
use super::{ArrayDescriptor, TableColumnFormat, TableElementFormat};
use crate::bin_table::Value;
fn format(tform: &str) -> TableColumnFormat {
TableColumnFormat::try_from(tform.to_string())
.unwrap_or_else(|error| panic!("{tform} should parse: {error}"))
}
#[test]
fn every_format_code_reports_its_standard_width() {
let cases = [
("1L", 1),
("8L", 8),
("1X", 1),
("8X", 1),
("9X", 2),
("16X", 2),
("17X", 3),
("1B", 1),
("4B", 4),
("1I", 2),
("4I", 8),
("1J", 4),
("1K", 8),
("1E", 4),
("1D", 8),
("1C", 8),
("3C", 24),
("1M", 16),
("3M", 48),
("20A", 20),
("60A20", 60),
];
for (tform, expected) in cases {
assert_eq!(format(tform).bytes_len(), expected, "TFORM {tform}");
}
}
#[test]
fn element_counts_match_the_repeat_count() {
assert_eq!(format("20A").len(), 1);
assert_eq!(format("60A20").len(), 3);
assert_eq!(format("4J").len(), 4);
assert_eq!(format("3C").len(), 3);
}
#[test]
fn single_precision_complex_decodes_both_components() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&1.5_f32.to_be_bytes());
bytes.extend_from_slice(&(-2.5_f32).to_be_bytes());
let Ok(Value::C32(values)) = format("1C").parse_into_value(&bytes, &[]) else {
panic!("a 1C column should decode to a complex value");
};
assert_eq!(values, vec![(1.5, -2.5)]);
}
#[test]
fn double_precision_complex_decodes_both_components() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&1.5_f64.to_be_bytes());
bytes.extend_from_slice(&(-2.5_f64).to_be_bytes());
let Ok(Value::M64(values)) = format("1M").parse_into_value(&bytes, &[]) else {
panic!("a 1M column should decode to a complex value");
};
assert_eq!(values, vec![(1.5, -2.5)]);
}
#[test]
fn a_string_array_splits_into_substrings_of_the_declared_width() {
let Ok(Value::StringArray(values)) =
format("15A5").parse_into_value(b"alphabeta gamma", &[])
else {
panic!("a 15A5 column should decode to a string array");
};
assert_eq!(values, vec!["alpha", "beta", "gamma"]);
}
#[test]
fn logical_columns_distinguish_true_from_false() {
let Ok(Value::Boolean(values)) = format("3L").parse_into_value(b"TF\0", &[]) else {
panic!("a 3L column should decode to logicals");
};
assert_eq!(values, vec![true, false, false]);
}
#[test]
fn a_row_too_short_for_the_column_is_an_error() {
let error = format("4J")
.parse_into_value(&[0, 0, 0, 1, 0, 0], &[])
.expect_err("a 16 byte column cannot be read from 6 bytes");
assert!(error.to_string().contains("needs 16 bytes"), "got: {error}");
}
#[test]
fn variable_length_array_formats_parse() {
assert_eq!(
format("1PJ(10)"),
TableColumnFormat::VariableLengthArray {
element: TableElementFormat::I32,
descriptor: ArrayDescriptor::P32,
max: 10,
}
);
assert_eq!(
format("1QE"),
TableColumnFormat::VariableLengthArray {
element: TableElementFormat::F32,
descriptor: ArrayDescriptor::Q64,
max: 0,
}
);
assert_eq!(format("1PJ(10)").bytes_len(), 8);
assert_eq!(format("1QE").bytes_len(), 16);
}
#[test]
fn a_variable_length_array_repeat_count_above_one_is_rejected() {
let error = TableColumnFormat::try_from("2PJ(10)".to_string())
.expect_err("a repeat count above one is invalid");
assert!(error.to_string().contains("repeat count"), "got: {error}");
}
#[test]
fn a_variable_length_array_reads_its_values_from_the_heap() {
let mut descriptor = Vec::new();
descriptor.extend_from_slice(&3_i32.to_be_bytes());
descriptor.extend_from_slice(&4_i32.to_be_bytes());
let mut heap = vec![0xFF; 4];
for value in [7_i32, 8, 9] {
heap.extend_from_slice(&value.to_be_bytes());
}
let Ok(Value::I32(values)) = format("1PJ(10)").parse_into_value(&descriptor, &heap) else {
panic!("a 1PJ column should decode to its heap values");
};
assert_eq!(values, vec![7, 8, 9]);
}
#[test]
fn an_empty_variable_length_array_points_nowhere() {
let descriptor = [0_u8; 8];
let Ok(Value::I32(values)) = format("1PJ(10)").parse_into_value(&descriptor, &[]) else {
panic!("a zero-length array should decode to no values");
};
assert!(values.is_empty());
}
#[test]
fn a_variable_length_array_past_the_end_of_the_heap_is_an_error() {
let mut descriptor = Vec::new();
descriptor.extend_from_slice(&3_i32.to_be_bytes());
descriptor.extend_from_slice(&100_i32.to_be_bytes());
let error = format("1PJ(10)")
.parse_into_value(&descriptor, &[0; 8])
.expect_err("an out of range descriptor cannot be followed");
assert!(error.to_string().contains("heap"), "got: {error}");
}
}