Skip to main content

fits_io/header/
table_column_format.rs

1use crate::bin_table::Value;
2use std::error::Error;
3use std::str::from_utf8;
4
5/// The type and repeat count of one binary table column, from its TFORMn card.
6///
7/// The contained count is the TFORMn repeat count `r`, not a byte length; use
8/// [`TableColumnFormat::bytes_len`] for the width of the field in the row.
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum TableColumnFormat {
11    /// `rA`: one string of `r` characters.
12    String(usize),
13    /// `rAw`: `r` characters holding `r / w` substrings of `w` characters.
14    StringArray(usize, usize),
15    /// `rL`: `r` logical values, one byte each.
16    Boolean(usize),
17    /// `rX`: `r` bits, packed into `ceil(r / 8)` bytes.
18    Bit(usize),
19    /// `rB`: `r` unsigned bytes.
20    U8(usize),
21    /// `rS`: `r` signed bytes.
22    I8(usize),
23    /// `rU`: `r` unsigned 16-bit integers.
24    U16(usize),
25    /// `rI`: `r` signed 16-bit integers.
26    I16(usize),
27    /// `rV`: `r` unsigned 32-bit integers.
28    U32(usize),
29    /// `rJ`: `r` signed 32-bit integers.
30    I32(usize),
31    /// `rK`: `r` signed 64-bit integers.
32    I64(usize),
33    /// `rE`: `r` single precision floats.
34    F32(usize),
35    /// `rD`: `r` double precision floats.
36    F64(usize),
37    /// `rC`: `r` single precision complex values, two `f32` each.
38    C32(usize),
39    /// `rM`: `r` double precision complex values, two `f64` each.
40    M64(usize),
41    /// `rPt(max)` or `rQt(max)`: a variable length array.
42    ///
43    /// The row itself holds only a descriptor saying how many values there are
44    /// and where in the table's heap they start; the values live in the heap,
45    /// after the last row.
46    VariableLengthArray {
47        /// The type of the values in the heap.
48        element: TableElementFormat,
49        /// Which descriptor the row carries: `P` for 32-bit, `Q` for 64-bit.
50        descriptor: ArrayDescriptor,
51        /// The `(max)` hint on the format, or 0 when it carries none. This is
52        /// the longest array the column promises, not the length of any
53        /// particular row's array.
54        max: usize,
55    },
56}
57
58/// Which of the two variable length array descriptors a column uses.
59#[derive(Debug, Clone, Copy, PartialEq)]
60pub enum ArrayDescriptor {
61    /// `P`: a pair of 32-bit integers, so a heap of up to 2 GiB.
62    P32,
63    /// `Q`: a pair of 64-bit integers, for heaps beyond that.
64    Q64,
65}
66
67impl ArrayDescriptor {
68    /// Width of the descriptor as stored in the row.
69    pub fn bytes_len(&self) -> usize {
70        match self {
71            ArrayDescriptor::P32 => 8,
72            ArrayDescriptor::Q64 => 16,
73        }
74    }
75
76    /// Reads a descriptor: how many elements the array has, and its byte offset
77    /// into the heap.
78    ///
79    /// Returns `None` for a descriptor that is truncated, or that describes a
80    /// negative count or offset.
81    pub fn read(&self, bytes: &[u8]) -> Option<(usize, usize)> {
82        let (count, offset) = match self {
83            ArrayDescriptor::P32 => {
84                let (count, offset) = bytes.get(..8)?.split_at(4);
85                (
86                    i32::from_be_bytes(count.try_into().ok()?) as i64,
87                    i32::from_be_bytes(offset.try_into().ok()?) as i64,
88                )
89            }
90            ArrayDescriptor::Q64 => {
91                let (count, offset) = bytes.get(..16)?.split_at(8);
92                (
93                    i64::from_be_bytes(count.try_into().ok()?),
94                    i64::from_be_bytes(offset.try_into().ok()?),
95                )
96            }
97        };
98
99        Some((usize::try_from(count).ok()?, usize::try_from(offset).ok()?))
100    }
101}
102
103/// The type of a single element, for the columns whose length is not fixed by
104/// their format.
105#[derive(Debug, Clone, Copy, PartialEq)]
106pub enum TableElementFormat {
107    /// `A`: one character.
108    Character,
109    /// `L`: a logical.
110    Boolean,
111    /// `X`: a bit.
112    Bit,
113    /// `B`: an unsigned byte.
114    U8,
115    /// `S`: a signed byte.
116    I8,
117    /// `U`: an unsigned 16-bit integer.
118    U16,
119    /// `I`: a signed 16-bit integer.
120    I16,
121    /// `V`: an unsigned 32-bit integer.
122    U32,
123    /// `J`: a signed 32-bit integer.
124    I32,
125    /// `K`: a signed 64-bit integer.
126    I64,
127    /// `E`: a single precision float.
128    F32,
129    /// `D`: a double precision float.
130    F64,
131    /// `C`: a single precision complex value.
132    C32,
133    /// `M`: a double precision complex value.
134    M64,
135}
136
137impl TableElementFormat {
138    /// The TFORMn code for this element type.
139    pub fn code(&self) -> char {
140        match self {
141            TableElementFormat::Character => 'A',
142            TableElementFormat::Boolean => 'L',
143            TableElementFormat::Bit => 'X',
144            TableElementFormat::U8 => 'B',
145            TableElementFormat::I8 => 'S',
146            TableElementFormat::U16 => 'U',
147            TableElementFormat::I16 => 'I',
148            TableElementFormat::U32 => 'V',
149            TableElementFormat::I32 => 'J',
150            TableElementFormat::I64 => 'K',
151            TableElementFormat::F32 => 'E',
152            TableElementFormat::F64 => 'D',
153            TableElementFormat::C32 => 'C',
154            TableElementFormat::M64 => 'M',
155        }
156    }
157
158    fn from_code(code: char) -> Option<Self> {
159        Some(match code {
160            'A' => TableElementFormat::Character,
161            'L' => TableElementFormat::Boolean,
162            'X' => TableElementFormat::Bit,
163            'B' => TableElementFormat::U8,
164            'S' => TableElementFormat::I8,
165            'U' => TableElementFormat::U16,
166            'I' => TableElementFormat::I16,
167            'V' => TableElementFormat::U32,
168            'J' => TableElementFormat::I32,
169            'K' => TableElementFormat::I64,
170            'E' => TableElementFormat::F32,
171            'D' => TableElementFormat::F64,
172            'C' => TableElementFormat::C32,
173            'M' => TableElementFormat::M64,
174            _ => return None,
175        })
176    }
177
178    /// This element type repeated `count` times, as a fixed-width column format.
179    ///
180    /// That is what a variable length array becomes once its descriptor has said
181    /// how long it actually is.
182    pub fn repeated(&self, count: usize) -> TableColumnFormat {
183        match self {
184            TableElementFormat::Character => TableColumnFormat::String(count),
185            TableElementFormat::Boolean => TableColumnFormat::Boolean(count),
186            TableElementFormat::Bit => TableColumnFormat::Bit(count),
187            TableElementFormat::U8 => TableColumnFormat::U8(count),
188            TableElementFormat::I8 => TableColumnFormat::I8(count),
189            TableElementFormat::U16 => TableColumnFormat::U16(count),
190            TableElementFormat::I16 => TableColumnFormat::I16(count),
191            TableElementFormat::U32 => TableColumnFormat::U32(count),
192            TableElementFormat::I32 => TableColumnFormat::I32(count),
193            TableElementFormat::I64 => TableColumnFormat::I64(count),
194            TableElementFormat::F32 => TableColumnFormat::F32(count),
195            TableElementFormat::F64 => TableColumnFormat::F64(count),
196            TableElementFormat::C32 => TableColumnFormat::C32(count),
197            TableElementFormat::M64 => TableColumnFormat::M64(count),
198        }
199    }
200}
201
202impl TableColumnFormat {
203    /// Decodes this column out of the front of `data`, which must be the
204    /// remainder of the row starting at this column's offset.
205    ///
206    /// `heap` is the table's heap, which only a variable length array column
207    /// reads from; pass an empty slice for a table that has none.
208    pub fn parse_into_value(&self, data: &[u8], heap: &[u8]) -> crate::Result<Value> {
209        if let TableColumnFormat::VariableLengthArray {
210            element,
211            descriptor,
212            ..
213        } = self
214        {
215            return self.parse_array_from_heap(*element, *descriptor, data, heap);
216        }
217
218        let width = self.bytes_len();
219
220        let bytes = data.get(..width).ok_or_else(|| {
221            crate::Error::DeserializationError(format!(
222                "Column of format {} needs {} bytes but only {} remain in the row",
223                String::from(*self),
224                width,
225                data.len()
226            ))
227        })?;
228
229        match self {
230            TableColumnFormat::String(_) => Ok(Value::String(decode_string(bytes)?)),
231
232            TableColumnFormat::StringArray(_, substring_width) => {
233                // TFORMn `rAw` is r characters total, split into substrings of w.
234                let substring_width = (*substring_width).max(1);
235
236                Ok(Value::StringArray(
237                    bytes
238                        .chunks(substring_width)
239                        .map(decode_string)
240                        .collect::<crate::Result<_>>()?,
241                ))
242            }
243
244            // A logical is stored as ASCII 'T' or 'F'; a zero byte means
245            // undefined. Anything else is not true.
246            TableColumnFormat::Boolean(_) => Ok(Value::Boolean(
247                bytes.iter().map(|byte| *byte == b'T').collect(),
248            )),
249
250            // The bits stay packed; `r` bits occupy ceil(r / 8) bytes.
251            TableColumnFormat::Bit(count) => Ok(Value::Bit {
252                bytes: bytes.to_vec(),
253                len: *count,
254            }),
255
256            TableColumnFormat::U8(_) => Ok(Value::U8(bytes.to_vec())),
257            TableColumnFormat::I8(_) => {
258                Ok(Value::I8(bytes.iter().map(|byte| *byte as i8).collect()))
259            }
260
261            TableColumnFormat::U16(_) => Ok(Value::U16(
262                bytes
263                    .as_chunks::<2>()
264                    .0
265                    .iter()
266                    .map(|value| u16::from_be_bytes(*value))
267                    .collect(),
268            )),
269            TableColumnFormat::I16(_) => Ok(Value::I16(
270                bytes
271                    .as_chunks::<2>()
272                    .0
273                    .iter()
274                    .map(|value| i16::from_be_bytes(*value))
275                    .collect(),
276            )),
277            TableColumnFormat::U32(_) => Ok(Value::U32(
278                bytes
279                    .as_chunks::<4>()
280                    .0
281                    .iter()
282                    .map(|value| u32::from_be_bytes(*value))
283                    .collect(),
284            )),
285            TableColumnFormat::I32(_) => Ok(Value::I32(
286                bytes
287                    .as_chunks::<4>()
288                    .0
289                    .iter()
290                    .map(|value| i32::from_be_bytes(*value))
291                    .collect(),
292            )),
293            TableColumnFormat::I64(_) => Ok(Value::I64(
294                bytes
295                    .as_chunks::<8>()
296                    .0
297                    .iter()
298                    .map(|value| i64::from_be_bytes(*value))
299                    .collect(),
300            )),
301            TableColumnFormat::F32(_) => Ok(Value::F32(
302                bytes
303                    .as_chunks::<4>()
304                    .0
305                    .iter()
306                    .map(|value| f32::from_be_bytes(*value))
307                    .collect(),
308            )),
309            TableColumnFormat::F64(_) => Ok(Value::F64(
310                bytes
311                    .as_chunks::<8>()
312                    .0
313                    .iter()
314                    .map(|value| f64::from_be_bytes(*value))
315                    .collect(),
316            )),
317
318            // Complex values are a real part followed by an imaginary part.
319            TableColumnFormat::C32(_) => Ok(Value::C32(
320                bytes
321                    .as_chunks::<8>()
322                    .0
323                    .iter()
324                    .map(|value| {
325                        let (real, imaginary) = value.split_at(4);
326                        (
327                            f32::from_be_bytes(real.try_into().expect("4 of 8 bytes")),
328                            f32::from_be_bytes(imaginary.try_into().expect("4 of 8 bytes")),
329                        )
330                    })
331                    .collect(),
332            )),
333            TableColumnFormat::M64(_) => Ok(Value::M64(
334                bytes
335                    .as_chunks::<16>()
336                    .0
337                    .iter()
338                    .map(|value| {
339                        let (real, imaginary) = value.split_at(8);
340                        (
341                            f64::from_be_bytes(real.try_into().expect("8 of 16 bytes")),
342                            f64::from_be_bytes(imaginary.try_into().expect("8 of 16 bytes")),
343                        )
344                    })
345                    .collect(),
346            )),
347
348            // Handled above, before the row slice is taken: this column's bytes
349            // are in the heap, not in the row.
350            TableColumnFormat::VariableLengthArray { .. } => {
351                unreachable!("a variable length array column is decoded from the heap")
352            }
353        }
354    }
355
356    /// Follows a variable length array's descriptor into the heap and decodes
357    /// the values it points at.
358    fn parse_array_from_heap(
359        &self,
360        element: TableElementFormat,
361        descriptor: ArrayDescriptor,
362        data: &[u8],
363        heap: &[u8],
364    ) -> crate::Result<Value> {
365        let (count, offset) = descriptor.read(data).ok_or_else(|| {
366            crate::Error::DeserializationError(format!(
367                "Column of format {} has an unreadable array descriptor",
368                String::from(*self)
369            ))
370        })?;
371
372        // A zero-length array is the normal way to say a row has no values for
373        // this column, and points nowhere.
374        let format = element.repeated(count);
375        if count == 0 {
376            return format.parse_into_value(&[], &[]);
377        }
378
379        let width = format.bytes_len();
380        let bytes = heap
381            .get(offset..)
382            .and_then(|heap| heap.get(..width))
383            .ok_or_else(|| {
384                crate::Error::DeserializationError(format!(
385                    "Column of format {} points at bytes {}..{} of a {} byte heap",
386                    String::from(*self),
387                    offset,
388                    offset + width,
389                    heap.len()
390                ))
391            })?;
392
393        format.parse_into_value(bytes, &[])
394    }
395
396    /// Width of this column in the row, in bytes.
397    ///
398    /// Every column's offset is the sum of the widths before it, so a wrong
399    /// answer here misaligns every following column.
400    pub fn bytes_len(&self) -> usize {
401        match self {
402            // `rA` and `rAw` both occupy r bytes; w only says how those bytes
403            // are divided into substrings.
404            TableColumnFormat::String(count) => *count,
405            TableColumnFormat::StringArray(count, _) => *count,
406
407            // r bits, rounded up to whole bytes.
408            TableColumnFormat::Bit(count) => count.div_ceil(8),
409
410            TableColumnFormat::Boolean(count) => *count,
411            TableColumnFormat::U8(count) => *count,
412            TableColumnFormat::I8(count) => *count,
413            TableColumnFormat::U16(count) => 2 * count,
414            TableColumnFormat::I16(count) => 2 * count,
415            TableColumnFormat::U32(count) => 4 * count,
416            TableColumnFormat::I32(count) => 4 * count,
417            TableColumnFormat::I64(count) => 8 * count,
418            TableColumnFormat::F32(count) => 4 * count,
419            TableColumnFormat::F64(count) => 8 * count,
420
421            // A complex value is a pair, so twice the width of its components.
422            TableColumnFormat::C32(count) => 8 * count,
423            TableColumnFormat::M64(count) => 16 * count,
424
425            // Only the descriptor sits in the row; the values are in the heap.
426            TableColumnFormat::VariableLengthArray { descriptor, .. } => descriptor.bytes_len(),
427        }
428    }
429
430    /// Number of elements this column holds.
431    pub fn len(&self) -> usize {
432        match self {
433            TableColumnFormat::String(_) => 1,
434            TableColumnFormat::StringArray(count, substring_width) => {
435                count / (*substring_width).max(1)
436            }
437            TableColumnFormat::Boolean(count)
438            | TableColumnFormat::Bit(count)
439            | TableColumnFormat::U8(count)
440            | TableColumnFormat::I8(count)
441            | TableColumnFormat::U16(count)
442            | TableColumnFormat::I16(count)
443            | TableColumnFormat::U32(count)
444            | TableColumnFormat::I32(count)
445            | TableColumnFormat::I64(count)
446            | TableColumnFormat::F32(count)
447            | TableColumnFormat::F64(count)
448            | TableColumnFormat::C32(count)
449            | TableColumnFormat::M64(count) => *count,
450
451            // A variable length array is a different length in every row, so the
452            // format can only report the upper bound it declares.
453            TableColumnFormat::VariableLengthArray { max, .. } => *max,
454        }
455    }
456
457    /// Whether this column holds no elements at all.
458    pub fn is_empty(&self) -> bool {
459        self.len() == 0
460    }
461}
462
463/// Decodes one fixed-width FITS character field, which is space padded and may
464/// be null terminated.
465fn decode_string(bytes: &[u8]) -> crate::Result<String> {
466    Ok(from_utf8(bytes)
467        .map_err(|e| crate::Error::DeserializationError(format!("Not valid UTF-8: {}", e)))?
468        .replace("\0", "")
469        .trim_ascii()
470        .to_string())
471}
472
473impl From<TableColumnFormat> for String {
474    fn from(value: TableColumnFormat) -> String {
475        match value {
476            TableColumnFormat::String(repeat) => format!("{}A", repeat),
477            TableColumnFormat::StringArray(repeat, items) => format!("{}A{}", repeat, items),
478            TableColumnFormat::Boolean(repeat) => format!("{}L", repeat),
479            TableColumnFormat::Bit(repeat) => format!("{}X", repeat),
480            TableColumnFormat::U8(repeat) => format!("{}B", repeat),
481            TableColumnFormat::I8(repeat) => format!("{}S", repeat),
482            TableColumnFormat::U16(repeat) => format!("{}U", repeat),
483            TableColumnFormat::I16(repeat) => format!("{}I", repeat),
484            TableColumnFormat::U32(repeat) => format!("{}V", repeat),
485            TableColumnFormat::I32(repeat) => format!("{}J", repeat),
486            TableColumnFormat::I64(repeat) => format!("{}K", repeat),
487            TableColumnFormat::F32(repeat) => format!("{}E", repeat),
488            TableColumnFormat::F64(repeat) => format!("{}D", repeat),
489            TableColumnFormat::C32(repeat) => format!("{}C", repeat),
490            TableColumnFormat::M64(repeat) => format!("{}M", repeat),
491            TableColumnFormat::VariableLengthArray {
492                element,
493                descriptor,
494                max,
495            } => {
496                let code = match descriptor {
497                    ArrayDescriptor::P32 => 'P',
498                    ArrayDescriptor::Q64 => 'Q',
499                };
500                format!("1{}{}({})", code, element.code(), max)
501            }
502        }
503    }
504}
505
506impl TryFrom<String> for TableColumnFormat {
507    type Error = Box<dyn Error + Send + Sync>;
508
509    fn try_from(value: String) -> Result<Self, Self::Error> {
510        let (repeat, format, items) = extract_parts(&value)?;
511        match format {
512            'A' => {
513                if items > 0 {
514                    Ok(TableColumnFormat::StringArray(repeat, items))
515                } else {
516                    Ok(TableColumnFormat::String(repeat))
517                }
518            }
519            'L' => Ok(TableColumnFormat::Boolean(repeat)),
520            'X' => Ok(TableColumnFormat::Bit(repeat)),
521            'B' => Ok(TableColumnFormat::U8(repeat)),
522            'S' => Ok(TableColumnFormat::I8(repeat)),
523            'I' => Ok(TableColumnFormat::I16(repeat)),
524            'U' => Ok(TableColumnFormat::U16(repeat)),
525            'J' => Ok(TableColumnFormat::I32(repeat)),
526            'V' => Ok(TableColumnFormat::U32(repeat)),
527            'K' => Ok(TableColumnFormat::I64(repeat)),
528            'E' => Ok(TableColumnFormat::F32(repeat)),
529            'D' => Ok(TableColumnFormat::F64(repeat)),
530            'C' => Ok(TableColumnFormat::C32(repeat)),
531            'M' => Ok(TableColumnFormat::M64(repeat)),
532            'P' | 'Q' => parse_variable_length_array(&value, repeat, format),
533            _ => Err(From::from(format!(
534                "Invalid TableColumnFormat value: {}",
535                value
536            ))),
537        }
538    }
539}
540
541/// Parses `rPt(max)` / `rQt(max)`, the two variable length array formats.
542///
543/// The standard allows only a repeat count of 0 or 1 here: the row holds one
544/// descriptor, and the count of values is in the descriptor rather than the
545/// format.
546fn parse_variable_length_array(
547    value: &str,
548    repeat: usize,
549    code: char,
550) -> Result<TableColumnFormat, Box<dyn Error + Send + Sync>> {
551    if repeat > 1 {
552        return Err(From::from(format!(
553            "A variable length array column holds one descriptor, so its repeat count must be 0 \
554             or 1, but {} says {}",
555            value, repeat
556        )));
557    }
558
559    let descriptor = match code {
560        'P' => ArrayDescriptor::P32,
561        _ => ArrayDescriptor::Q64,
562    };
563
564    // Everything after the leading repeat count and the P or Q.
565    let rest = value
566        .trim_start_matches(|c: char| c.is_ascii_digit())
567        .get(1..)
568        .unwrap_or_default();
569
570    let (element_code, rest) = {
571        let mut chars = rest.chars();
572        let element_code = chars.next().ok_or_else(|| {
573            format!(
574                "Variable length array format {} names no element type",
575                value
576            )
577        })?;
578        (element_code, chars.as_str())
579    };
580
581    let element = TableElementFormat::from_code(element_code).ok_or_else(|| {
582        format!(
583            "Variable length array format {} has an invalid element type: {}",
584            value, element_code
585        )
586    })?;
587
588    // The `(max)` suffix is a hint, and the standard makes it optional.
589    let max = match rest
590        .trim()
591        .strip_prefix('(')
592        .and_then(|rest| rest.strip_suffix(')'))
593    {
594        Some(max) => max.trim().parse::<usize>().map_err(|_| {
595            format!(
596                "Variable length array format {} has an invalid maximum",
597                value
598            )
599        })?,
600        None if rest.trim().is_empty() => 0,
601        None => {
602            return Err(From::from(format!(
603                "Trailing characters in variable length array format {}",
604                value
605            )));
606        }
607    };
608
609    Ok(TableColumnFormat::VariableLengthArray {
610        element,
611        descriptor,
612        max,
613    })
614}
615
616fn extract_parts(value: &str) -> Result<(usize, char, usize), Box<dyn Error + Send + Sync>> {
617    let mut chars = value.chars().peekable();
618    let mut repeat_str = String::new();
619    while let Some(c) = chars.peek() {
620        if c.is_ascii_digit() {
621            repeat_str.push(*c);
622            chars.next();
623        } else {
624            break;
625        }
626    }
627
628    let repeat = if repeat_str.is_empty() {
629        1
630    } else {
631        repeat_str
632            .parse::<usize>()
633            .map_err(|_| "Invalid repeat count")?
634    };
635
636    // Parse type code
637    let code = chars
638        .next()
639        .ok_or_else(|| "Missing format code".to_string())?;
640
641    let mut width_str = String::new();
642    while let Some(c) = chars.peek() {
643        if c.is_ascii_digit() {
644            width_str.push(*c);
645            chars.next();
646        } else {
647            break;
648        }
649    }
650
651    let width = if width_str.is_empty() {
652        0
653    } else {
654        width_str
655            .parse::<usize>()
656            .map_err(|_| "Invalid string width")?
657    };
658
659    Ok((repeat, code, width))
660}
661
662#[cfg(test)]
663mod tests {
664    use super::{ArrayDescriptor, TableColumnFormat, TableElementFormat};
665    use crate::bin_table::Value;
666
667    fn format(tform: &str) -> TableColumnFormat {
668        TableColumnFormat::try_from(tform.to_string())
669            .unwrap_or_else(|error| panic!("{tform} should parse: {error}"))
670    }
671
672    /// The byte width of every TFORMn code, per the FITS standard. A wrong width
673    /// here shifts every following column in the row.
674    #[test]
675    fn every_format_code_reports_its_standard_width() {
676        let cases = [
677            ("1L", 1),
678            ("8L", 8),
679            // r bits rounded up to whole bytes.
680            ("1X", 1),
681            ("8X", 1),
682            ("9X", 2),
683            ("16X", 2),
684            ("17X", 3),
685            ("1B", 1),
686            ("4B", 4),
687            ("1I", 2),
688            ("4I", 8),
689            ("1J", 4),
690            ("1K", 8),
691            ("1E", 4),
692            ("1D", 8),
693            // A complex value is a pair of components.
694            ("1C", 8),
695            ("3C", 24),
696            ("1M", 16),
697            ("3M", 48),
698            // rA and rAw both occupy r bytes.
699            ("20A", 20),
700            ("60A20", 60),
701        ];
702
703        for (tform, expected) in cases {
704            assert_eq!(format(tform).bytes_len(), expected, "TFORM {tform}");
705        }
706    }
707
708    #[test]
709    fn element_counts_match_the_repeat_count() {
710        assert_eq!(format("20A").len(), 1);
711        assert_eq!(format("60A20").len(), 3);
712        assert_eq!(format("4J").len(), 4);
713        assert_eq!(format("3C").len(), 3);
714    }
715
716    #[test]
717    fn single_precision_complex_decodes_both_components() {
718        let mut bytes = Vec::new();
719        bytes.extend_from_slice(&1.5_f32.to_be_bytes());
720        bytes.extend_from_slice(&(-2.5_f32).to_be_bytes());
721
722        let Ok(Value::C32(values)) = format("1C").parse_into_value(&bytes, &[]) else {
723            panic!("a 1C column should decode to a complex value");
724        };
725
726        assert_eq!(values, vec![(1.5, -2.5)]);
727    }
728
729    #[test]
730    fn double_precision_complex_decodes_both_components() {
731        let mut bytes = Vec::new();
732        bytes.extend_from_slice(&1.5_f64.to_be_bytes());
733        bytes.extend_from_slice(&(-2.5_f64).to_be_bytes());
734
735        let Ok(Value::M64(values)) = format("1M").parse_into_value(&bytes, &[]) else {
736            panic!("a 1M column should decode to a complex value");
737        };
738
739        assert_eq!(values, vec![(1.5, -2.5)]);
740    }
741
742    #[test]
743    fn a_string_array_splits_into_substrings_of_the_declared_width() {
744        // 15A5 is 15 characters holding three 5-character substrings, each
745        // space padded to its full width.
746        let Ok(Value::StringArray(values)) =
747            format("15A5").parse_into_value(b"alphabeta gamma", &[])
748        else {
749            panic!("a 15A5 column should decode to a string array");
750        };
751
752        assert_eq!(values, vec!["alpha", "beta", "gamma"]);
753    }
754
755    #[test]
756    fn logical_columns_distinguish_true_from_false() {
757        // 'F' is a non-zero byte, so a zero test reports it as true.
758        let Ok(Value::Boolean(values)) = format("3L").parse_into_value(b"TF\0", &[]) else {
759            panic!("a 3L column should decode to logicals");
760        };
761
762        assert_eq!(values, vec![true, false, false]);
763    }
764
765    #[test]
766    fn a_row_too_short_for_the_column_is_an_error() {
767        let error = format("4J")
768            .parse_into_value(&[0, 0, 0, 1, 0, 0], &[])
769            .expect_err("a 16 byte column cannot be read from 6 bytes");
770
771        assert!(error.to_string().contains("needs 16 bytes"), "got: {error}");
772    }
773
774    #[test]
775    fn variable_length_array_formats_parse() {
776        assert_eq!(
777            format("1PJ(10)"),
778            TableColumnFormat::VariableLengthArray {
779                element: TableElementFormat::I32,
780                descriptor: ArrayDescriptor::P32,
781                max: 10,
782            }
783        );
784
785        // The `(max)` hint is optional, and Q is the 64-bit descriptor.
786        assert_eq!(
787            format("1QE"),
788            TableColumnFormat::VariableLengthArray {
789                element: TableElementFormat::F32,
790                descriptor: ArrayDescriptor::Q64,
791                max: 0,
792            }
793        );
794
795        // Only the descriptor occupies space in the row.
796        assert_eq!(format("1PJ(10)").bytes_len(), 8);
797        assert_eq!(format("1QE").bytes_len(), 16);
798    }
799
800    #[test]
801    fn a_variable_length_array_repeat_count_above_one_is_rejected() {
802        // A row holds exactly one descriptor, so `2PJ` is not a thing.
803        let error = TableColumnFormat::try_from("2PJ(10)".to_string())
804            .expect_err("a repeat count above one is invalid");
805
806        assert!(error.to_string().contains("repeat count"), "got: {error}");
807    }
808
809    #[test]
810    fn a_variable_length_array_reads_its_values_from_the_heap() {
811        // Descriptor: three elements, starting at byte 4 of the heap.
812        let mut descriptor = Vec::new();
813        descriptor.extend_from_slice(&3_i32.to_be_bytes());
814        descriptor.extend_from_slice(&4_i32.to_be_bytes());
815
816        let mut heap = vec![0xFF; 4];
817        for value in [7_i32, 8, 9] {
818            heap.extend_from_slice(&value.to_be_bytes());
819        }
820
821        let Ok(Value::I32(values)) = format("1PJ(10)").parse_into_value(&descriptor, &heap) else {
822            panic!("a 1PJ column should decode to its heap values");
823        };
824
825        assert_eq!(values, vec![7, 8, 9]);
826    }
827
828    #[test]
829    fn an_empty_variable_length_array_points_nowhere() {
830        let descriptor = [0_u8; 8];
831
832        let Ok(Value::I32(values)) = format("1PJ(10)").parse_into_value(&descriptor, &[]) else {
833            panic!("a zero-length array should decode to no values");
834        };
835
836        assert!(values.is_empty());
837    }
838
839    #[test]
840    fn a_variable_length_array_past_the_end_of_the_heap_is_an_error() {
841        let mut descriptor = Vec::new();
842        descriptor.extend_from_slice(&3_i32.to_be_bytes());
843        descriptor.extend_from_slice(&100_i32.to_be_bytes());
844
845        let error = format("1PJ(10)")
846            .parse_into_value(&descriptor, &[0; 8])
847            .expect_err("an out of range descriptor cannot be followed");
848
849        assert!(error.to_string().contains("heap"), "got: {error}");
850    }
851}