Skip to main content

asdf_core/core/
elements.rs

1//! Decoding an ndarray's bytes into typed elements, and inlining them back
2//! into the tree.
3//!
4//! Inlining is what the ASDF Standard's reference corpus asks a reader to do
5//! before comparing a `.asdf` file against its expected `.yaml`: turn every
6//! block-backed array into the nested sequences the expected output carries.
7
8use asdf_yaml::{CollectionStyle, Document, Node, NodeData, NodeId, ScalarStyle};
9
10use crate::core::datatype::{ByteOrder, Datatype, ScalarType};
11use crate::core::ndarray::{Ndarray, Source};
12use crate::error::{Result, err};
13
14/// One decoded array element.
15#[derive(Clone, PartialEq, Debug)]
16pub enum Element {
17    /// A signed integer.
18    Int(i64),
19    /// An unsigned integer.
20    Uint(u64),
21    /// A float, including the half- and single-precision types widened to
22    /// `f64`.
23    Float(f64),
24    /// A boolean, from `bool8`.
25    Bool(bool),
26    /// Fixed-length text, with trailing NULs trimmed.
27    Text(String),
28    /// A complex number.
29    Complex(f64, f64),
30    /// One record of a compound array.
31    Record(Vec<Element>),
32}
33
34/// Read a big- or little-endian integer of `n` bytes.
35fn read_uint(bytes: &[u8], order: ByteOrder) -> u64 {
36    let mut acc = 0u64;
37    if order == ByteOrder::Big {
38        for b in bytes {
39            acc = (acc << 8) | u64::from(*b);
40        }
41    } else {
42        for b in bytes.iter().rev() {
43            acc = (acc << 8) | u64::from(*b);
44        }
45    }
46    acc
47}
48
49/// Sign-extend an `n`-byte two's-complement value.
50fn sign_extend(value: u64, bytes: usize) -> i64 {
51    let bits = bytes * 8;
52    if bits >= 64 {
53        return value as i64;
54    }
55    let shift = 64 - bits;
56    ((value << shift) as i64) >> shift
57}
58
59/// The order to actually use for a field: its own if set, else the array's.
60fn effective_order(field: ByteOrder, array: ByteOrder) -> ByteOrder {
61    match field {
62        ByteOrder::Big | ByteOrder::Little => field,
63        // The schema's default when nothing says otherwise.
64        _ => match array {
65            ByteOrder::Big | ByteOrder::Little => array,
66            _ => ByteOrder::Little,
67        },
68    }
69}
70
71/// Decode a single element of `datatype` from the front of `bytes`.
72fn decode_one(datatype: &Datatype, bytes: &[u8], array_order: ByteOrder) -> Result<Element> {
73    if datatype.is_structured() {
74        let mut fields = Vec::with_capacity(datatype.fields.len());
75        let mut offset = 0usize;
76        for field in &datatype.fields {
77            let width = field.datatype.item_size() as usize;
78            let slice = bytes.get(offset..offset + width).ok_or_else(|| {
79                err!(UnexpectedEof, "compound element truncated at field offset {offset}")
80            })?;
81            fields.push(decode_one(&field.datatype, slice, array_order)?);
82            offset += width;
83        }
84        return Ok(Element::Record(fields));
85    }
86
87    let order = effective_order(datatype.byteorder, array_order);
88    let width = datatype.item_size() as usize;
89    let raw = bytes.get(..width).ok_or_else(|| {
90        err!(UnexpectedEof, "element needs {width} bytes, {} available", bytes.len())
91    })?;
92
93    Ok(match datatype.scalar {
94        ScalarType::Bool8 => Element::Bool(raw[0] != 0),
95
96        ScalarType::Uint8 | ScalarType::Uint16 | ScalarType::Uint32 | ScalarType::Uint64 => {
97            Element::Uint(read_uint(raw, order))
98        }
99
100        ScalarType::Int8 | ScalarType::Int16 | ScalarType::Int32 | ScalarType::Int64 => {
101            Element::Int(sign_extend(read_uint(raw, order), width))
102        }
103
104        ScalarType::Float16 => {
105            let bits = read_uint(raw, order) as u16;
106            Element::Float(f64::from(half::f16::from_bits(bits)))
107        }
108        ScalarType::Float32 => {
109            let bits = read_uint(raw, order) as u32;
110            Element::Float(f64::from(f32::from_bits(bits)))
111        }
112        ScalarType::Float64 => Element::Float(f64::from_bits(read_uint(raw, order))),
113
114        ScalarType::Complex64 => {
115            let re = f32::from_bits(read_uint(&raw[..4], order) as u32);
116            let im = f32::from_bits(read_uint(&raw[4..], order) as u32);
117            Element::Complex(f64::from(re), f64::from(im))
118        }
119        ScalarType::Complex128 => {
120            let re = f64::from_bits(read_uint(&raw[..8], order));
121            let im = f64::from_bits(read_uint(&raw[8..], order));
122            Element::Complex(re, im)
123        }
124
125        ScalarType::Ascii => {
126            // Fixed-length text is NUL-padded to its declared width.
127            let end = raw.iter().position(|b| *b == 0).unwrap_or(raw.len());
128            Element::Text(String::from_utf8_lossy(&raw[..end]).into_owned())
129        }
130        ScalarType::Ucs4 => {
131            let mut out = String::new();
132            let (quads, _) = raw.as_chunks::<4>();
133            for chunk in quads {
134                let cp = read_uint(chunk, order) as u32;
135                if cp == 0 {
136                    break;
137                }
138                out.push(char::from_u32(cp).unwrap_or(char::REPLACEMENT_CHARACTER));
139            }
140            Element::Text(out)
141        }
142
143        ScalarType::Unknown | ScalarType::Structured => {
144            return Err(err!(
145                InvalidArgument,
146                "cannot decode a {} element",
147                datatype.scalar.name()
148            ));
149        }
150    })
151}
152
153/// Decode every element of an array, in C order.
154///
155/// `shape` must be fully resolved. Strides, when present, are honoured, so a
156/// tile view or a Fortran-order array reads correctly.
157pub fn decode_all(nd: &Ndarray, shape: &[u64], bytes: &[u8]) -> Result<Vec<Element>> {
158    let item = nd.datatype.item_size();
159    if item == 0 {
160        return Err(err!(InvalidArgument, "cannot decode elements of zero width"));
161    }
162
163    let count = crate::core::ndarray::element_count(shape)?;
164
165    // Refuse before allocating, not after. The shape is attacker-controlled
166    // and `count` here is a count of `Element`s, each several times wider
167    // than a stored element, so a shape the block cannot possibly hold turns
168    // straight into an allocation nothing justifies -- and an allocation
169    // that large aborts the process rather than unwinding, which no
170    // `catch_unwind` at the C boundary can intercept. A file that claims
171    // more elements than its own block has bytes for is simply wrong.
172    let needed = count
173        .checked_mul(item)
174        .and_then(|n| n.checked_add(nd.offset))
175        .ok_or_else(|| err!(OverLimit, "array's extent does not fit in 64 bits"))?;
176    if needed > bytes.len() as u64 {
177        return Err(err!(
178            UnexpectedEof,
179            "array of {count} elements needs {needed} bytes but the block holds {}",
180            bytes.len()
181        ));
182    }
183
184    let count = usize::try_from(count)
185        .map_err(|_| err!(OverLimit, "array has too many elements for this platform"))?;
186
187    let strides = match &nd.strides {
188        Some(s) if s.len() == shape.len() => s.clone(),
189        Some(s) => {
190            return Err(err!(
191                InvalidArgument,
192                "strides have {} entries but the shape has {}",
193                s.len(),
194                shape.len()
195            ));
196        }
197        None => Ndarray::c_strides(shape, item)
198            .ok_or_else(|| err!(OverLimit, "shape {shape:?} is too large to stride"))?,
199    };
200
201    let base = usize::try_from(nd.offset)
202        .map_err(|_| err!(InvalidArgument, "ndarray offset overflows this platform"))?;
203
204    let mut out = Vec::with_capacity(count);
205    let mut index = vec![0u64; shape.len()];
206
207    for _ in 0..count {
208        // Byte position of this element, from the per-dimension strides.
209        let mut pos = base as i64;
210        for (dim, idx) in index.iter().enumerate() {
211            pos += strides[dim] * (*idx as i64);
212        }
213        let pos = usize::try_from(pos)
214            .map_err(|_| err!(InvalidArgument, "strides address a negative offset"))?;
215
216        let slice = bytes.get(pos..).ok_or_else(|| {
217            err!(UnexpectedEof, "element at byte {pos} is past the end of the block")
218        })?;
219        out.push(decode_one(&nd.datatype, slice, nd.byteorder)?);
220
221        // Odometer step, last dimension fastest.
222        for dim in (0..shape.len()).rev() {
223            index[dim] += 1;
224            if index[dim] < shape[dim] {
225                break;
226            }
227            index[dim] = 0;
228        }
229    }
230    Ok(out)
231}
232
233/// Decode an array whose data is inline in the tree rather than in a block.
234///
235/// The elements are already text in the document, so this is the mirror of
236/// [`inline_ndarray`]: walk the nested sequences under `data` in row-major
237/// order and read each scalar as the array's datatype.
238///
239/// The descent is driven by `shape`, not by how deeply the sequences happen
240/// to nest. That distinction matters for a compound array, whose innermost
241/// "sequence" is one record's fields rather than another dimension.
242///
243/// A `mask`ed value written as `null` decodes to the datatype's zero, since
244/// [`Element`] carries no missing-value marker; `mask` itself is available on
245/// the [`Ndarray`] for a caller that needs to know which those were.
246pub fn decode_inline(doc: &Document, array: &Ndarray, shape: &[u64]) -> Result<Vec<Element>> {
247    let Source::Inline(root) = array.source else {
248        return Err(err!(InvalidArgument, "this array's data is not inline"));
249    };
250
251    let expected = crate::core::ndarray::element_count(shape)?;
252    // Inline data is bounded by the tree that carries it, so the shape is
253    // checked against what is actually there rather than trusted to size a
254    // reservation. `collect_inline` grows the vector as it walks.
255    let mut out = Vec::new();
256    collect_inline(doc, root, &array.datatype, shape, &mut out)?;
257
258    if out.len() as u64 != expected {
259        return Err(err!(
260            InvalidArgument,
261            "inline data holds {} elements but the shape calls for {expected}",
262            out.len()
263        ));
264    }
265    Ok(out)
266}
267
268/// Walk `shape.len()` levels of sequences, reading each leaf as `datatype`.
269fn collect_inline(
270    doc: &Document,
271    node: NodeId,
272    datatype: &Datatype,
273    shape: &[u64],
274    out: &mut Vec<Element>,
275) -> Result<()> {
276    let resolved = doc.resolve(node);
277
278    let Some((dim, rest)) = shape.split_first() else {
279        // Past the last dimension: this is one element.
280        out.push(leaf_element(doc, resolved, datatype)?);
281        return Ok(());
282    };
283
284    let items = doc.sequence_items(resolved).map(<[_]>::to_vec).ok_or_else(|| {
285        err!(InvalidArgument, "inline array data is not nested {} deep", shape.len())
286    })?;
287    if items.len() as u64 != *dim {
288        return Err(err!(
289            InvalidArgument,
290            "inline dimension holds {} entries but the shape calls for {dim}",
291            items.len()
292        ));
293    }
294    for item in items {
295        collect_inline(doc, item, datatype, rest, out)?;
296    }
297    Ok(())
298}
299
300/// Read one element: a scalar, or a record for a compound datatype.
301fn leaf_element(doc: &Document, node: NodeId, datatype: &Datatype) -> Result<Element> {
302    if !datatype.fields.is_empty() {
303        let items = doc.sequence_items(node).map(<[_]>::to_vec).ok_or_else(|| {
304            err!(InvalidArgument, "a compound element must be a sequence of its fields")
305        })?;
306        if items.len() != datatype.fields.len() {
307            return Err(err!(
308                InvalidArgument,
309                "a compound element holds {} values but the datatype has {} fields",
310                items.len(),
311                datatype.fields.len()
312            ));
313        }
314        let mut record = Vec::with_capacity(items.len());
315        for (item, field) in items.iter().zip(datatype.fields.iter()) {
316            record.push(leaf_element(doc, doc.resolve(*item), &field.datatype)?);
317        }
318        return Ok(Element::Record(record));
319    }
320
321    let text = doc
322        .resolved(node)
323        .as_str()
324        .ok_or_else(|| err!(InvalidArgument, "inline array data holds a non-scalar leaf"))?;
325    scalar_element(text, datatype.scalar)
326}
327
328/// Read one inline scalar as `scalar`.
329fn scalar_element(text: &str, scalar: ScalarType) -> Result<Element> {
330    use ScalarType as S;
331
332    // A masked element is written `null`; there is no missing marker in
333    // `Element`, so it reads as the type's zero.
334    if matches!(text, "null" | "~" | "") {
335        return Ok(match scalar {
336            S::Float16 | S::Float32 | S::Float64 => Element::Float(0.0),
337            S::Complex64 | S::Complex128 => Element::Complex(0.0, 0.0),
338            S::Bool8 => Element::Bool(false),
339            S::Ascii | S::Ucs4 => Element::Text(String::new()),
340            S::Uint8 | S::Uint16 | S::Uint32 | S::Uint64 => Element::Uint(0),
341            _ => Element::Int(0),
342        });
343    }
344
345    let bad = |what: &str| err!(InvalidArgument, "inline {what} value {text:?} does not parse");
346    match scalar {
347        S::Uint8 | S::Uint16 | S::Uint32 | S::Uint64 => {
348            Ok(Element::Uint(text.parse::<u64>().map_err(|_| bad("unsigned"))?))
349        }
350        S::Int8 | S::Int16 | S::Int32 | S::Int64 => {
351            Ok(Element::Int(text.parse::<i64>().map_err(|_| bad("integer"))?))
352        }
353        S::Float16 | S::Float32 | S::Float64 => Ok(Element::Float(parse_inline_float(text)?)),
354        S::Complex64 | S::Complex128 => {
355            let (re, im) = parse_inline_complex(text)?;
356            Ok(Element::Complex(re, im))
357        }
358        S::Bool8 => Ok(Element::Bool(matches!(text, "true" | "True" | "1"))),
359        S::Ascii | S::Ucs4 => Ok(Element::Text(text.to_string())),
360        S::Unknown | S::Structured => {
361            Err(err!(InvalidArgument, "inline data needs a known scalar datatype"))
362        }
363    }
364}
365
366/// Parse a float, accepting YAML's non-finite spellings and Python's.
367fn parse_inline_float(text: &str) -> Result<f64> {
368    match text {
369        ".nan" | ".NaN" | ".NAN" | "nan" => return Ok(f64::NAN),
370        ".inf" | ".Inf" | ".INF" | "inf" => return Ok(f64::INFINITY),
371        "-.inf" | "-.Inf" | "-.INF" | "-inf" => return Ok(f64::NEG_INFINITY),
372        _ => {}
373    }
374    text.parse::<f64>()
375        .map_err(|_| err!(InvalidArgument, "inline float value {text:?} does not parse"))
376}
377
378/// Parse a complex number in the spellings the `core/complex` schema allows.
379///
380/// Both `(1+2j)` and `1+2j` are valid, as is a pure imaginary `3j` or a pure
381/// real `1`. The imaginary unit may be `i` or `j`, either case.
382fn parse_inline_complex(text: &str) -> Result<(f64, f64)> {
383    let body = text.trim();
384    let body = body.strip_prefix('(').map_or(body, |rest| rest.strip_suffix(')').unwrap_or(rest));
385
386    let imaginary_unit = |c: char| matches!(c, 'i' | 'I' | 'j' | 'J');
387    let Some(unit) = body.char_indices().rev().find(|(_, c)| imaginary_unit(*c)) else {
388        // No imaginary part at all: a plain real number.
389        return Ok((parse_inline_float(body)?, 0.0));
390    };
391    // The unit must be last; anything after it is not a complex number.
392    if unit.0 + unit.1.len_utf8() != body.len() {
393        return Err(err!(InvalidArgument, "inline complex value {text:?} does not parse"));
394    }
395    let without_unit = &body[..unit.0];
396
397    // Split off the imaginary part at the sign that separates the two, which
398    // is the last `+`/`-` not part of an exponent and not the leading sign.
399    let split = without_unit
400        .char_indices()
401        .rev()
402        .find(|(index, c)| {
403            (*c == '+' || *c == '-')
404                && *index > 0
405                && !matches!(without_unit.as_bytes()[index - 1], b'e' | b'E')
406        })
407        .map(|(index, _)| index);
408
409    match split {
410        None => Ok((0.0, parse_inline_float(without_unit)?)),
411        Some(index) => {
412            let (real, imaginary) = without_unit.split_at(index);
413            // The imaginary part keeps its sign; a bare sign means one.
414            let imaginary = match imaginary {
415                "+" => "1",
416                "-" => "-1",
417                other => other,
418            };
419            Ok((parse_inline_float(real)?, parse_inline_float(imaginary)?))
420        }
421    }
422}
423
424/// The tag Python asdf puts on every inline complex value.
425const COMPLEX_TAG: &str = "tag:stsci.edu:asdf/core/complex-1.0.0";
426
427/// Format a float the way libasdf's emitter does.
428///
429/// `%.17g` for doubles, with YAML's own spellings for the non-finite values.
430pub fn format_float(value: f64) -> String {
431    if value.is_nan() {
432        return ".nan".to_string();
433    }
434    if value.is_infinite() {
435        return if value.is_sign_negative() { "-.inf".into() } else { ".inf".into() };
436    }
437    // Shortest representation that round-trips, which is what Rust's
438    // formatter gives and what a reader will parse back identically.
439    let mut s = format!("{value}");
440    if !s.contains('.') && !s.contains('e') && !s.contains("inf") && !s.contains("nan") {
441        s.push_str(".0");
442    }
443    s
444}
445
446/// Render one element as a tree node.
447fn element_to_node(doc: &mut Document, element: &Element) -> NodeId {
448    match element {
449        Element::Int(v) => doc.add_scalar(v.to_string()),
450        Element::Uint(v) => doc.add_scalar(v.to_string()),
451        Element::Bool(v) => doc.add_scalar(if *v { "true" } else { "false" }),
452        Element::Float(v) => doc.add_scalar(format_float(*v)),
453        // Text is quoted so it round-trips as a string rather than being
454        // re-resolved as a number.
455        Element::Text(s) => doc.add_scalar_styled(s.clone(), ScalarStyle::SingleQuoted),
456        Element::Complex(re, im) => {
457            // The core/complex schema leaves the spelling open; Python asdf
458            // writes CPython's `repr(complex)` and tags each value, so that
459            // is the canonical form to reproduce.
460            let node = Node::scalar(crate::core::pyrepr::repr_complex(*re, *im))
461                .with_tag(asdf_yaml::Tag::parse(COMPLEX_TAG));
462            doc.add(node)
463        }
464        Element::Record(fields) => {
465            let items: Vec<NodeId> = fields.iter().map(|f| element_to_node(doc, f)).collect();
466            doc.add_sequence(items)
467        }
468    }
469}
470
471/// Build nested sequences for `elements` laid out in `shape`.
472pub fn nest(doc: &mut Document, elements: &[Element], shape: &[u64]) -> NodeId {
473    fn build(
474        doc: &mut Document,
475        elements: &[Element],
476        shape: &[u64],
477        cursor: &mut usize,
478    ) -> NodeId {
479        match shape.split_first() {
480            None => {
481                let node = element_to_node(doc, &elements[*cursor]);
482                *cursor += 1;
483                node
484            }
485            Some((dim, rest)) => {
486                let mut items = Vec::with_capacity(*dim as usize);
487                for _ in 0..*dim {
488                    items.push(build(doc, elements, rest, cursor));
489                }
490                let id = doc.add_sequence(items);
491                // Inline data reads better in flow style, which is how both
492                // other implementations write it.
493                if let NodeData::Sequence { style, .. } = &mut doc.node_mut(id).data {
494                    *style = CollectionStyle::Flow;
495                }
496                id
497            }
498        }
499    }
500
501    let mut cursor = 0;
502    build(doc, elements, shape, &mut cursor)
503}
504
505/// Replace an ndarray's `source` with the inline `data` it stands for.
506///
507/// This is the transformation the reference corpus prescribes. The array's
508/// `byteorder`, `offset` and `strides` describe how bytes sit in a block and
509/// become meaningless once the data is inline, so they are removed too.
510pub fn inline_ndarray(
511    doc: &mut Document,
512    id: NodeId,
513    elements: &[Element],
514    shape: &[u64],
515) -> Result<()> {
516    let data = nest(doc, elements, shape);
517    let target = doc.resolve(id);
518
519    if !doc.node(target).is_mapping() {
520        // The bare-sequence shorthand is already inline.
521        return Ok(());
522    }
523
524    doc.mapping_remove(target, "source");
525    for key in ["byteorder", "offset", "strides"] {
526        doc.mapping_remove(target, key);
527    }
528    // A compound datatype's fields carry their own byteorder, which is just
529    // as meaningless once the data is inline.
530    if let Some(dt) = doc.mapping_get(target, "datatype")
531        && let Some(fields) = doc.sequence_items(dt).map(<[_]>::to_vec)
532    {
533        for field in fields {
534            let field = doc.resolve(field);
535            if doc.node(field).is_mapping() {
536                doc.mapping_remove(field, "byteorder");
537            }
538        }
539    }
540    doc.mapping_set(target, "data", data);
541
542    // Record the shape the data actually has, replacing any '*'.
543    let dims: Vec<NodeId> = shape.iter().map(|d| doc.add_scalar(d.to_string())).collect();
544    let shape_node = doc.add_sequence(dims);
545    if let NodeData::Sequence { style, .. } = &mut doc.node_mut(shape_node).data {
546        *style = CollectionStyle::Flow;
547    }
548    doc.mapping_set(target, "shape", shape_node);
549    Ok(())
550}
551
552/// Build a node holding an element, for tests and callers wanting one value.
553pub fn element_node(doc: &mut Document, element: &Element) -> NodeId {
554    element_to_node(doc, element)
555}
556
557/// A node with a tag applied.
558pub fn tagged(doc: &mut Document, node: Node, tag: asdf_yaml::Tag) -> NodeId {
559    doc.add(node.with_tag(tag))
560}
561
562#[cfg(test)]
563mod tests {
564    use super::*;
565    use asdf_yaml::parse_document;
566
567    fn ndarray(yaml: &str) -> Ndarray {
568        let doc = parse_document(yaml).unwrap();
569        let root = doc.root().unwrap();
570        Ndarray::parse(&doc, doc.mapping_get(root, "a").unwrap()).unwrap()
571    }
572
573    #[test]
574    fn inline_integers_decode_from_the_tree() {
575        let doc = parse_document(
576            "a:\n  data: [[1, 2, 3], [4, 5, 6]]\n  datatype: int32\n  shape: [2, 3]\n",
577        )
578        .unwrap();
579        let root = doc.root().unwrap();
580        let nd = Ndarray::parse(&doc, doc.mapping_get(root, "a").unwrap()).unwrap();
581        let shape = nd.resolved_shape(None).unwrap();
582        assert_eq!(shape, vec![2, 3]);
583
584        let els = decode_inline(&doc, &nd, &shape).unwrap();
585        assert_eq!(
586            els,
587            (1..=6).map(Element::Int).collect::<Vec<_>>(),
588            "row-major order, flattened"
589        );
590    }
591
592    #[test]
593    fn inline_floats_accept_yamls_non_finite_spellings() {
594        let doc = parse_document(
595            "a:\n  data: [1.5, .inf, -.inf, .nan]\n  datatype: float64\n  shape: [4]\n",
596        )
597        .unwrap();
598        let root = doc.root().unwrap();
599        let nd = Ndarray::parse(&doc, doc.mapping_get(root, "a").unwrap()).unwrap();
600        let els = decode_inline(&doc, &nd, &[4]).unwrap();
601
602        assert_eq!(els[0], Element::Float(1.5));
603        assert_eq!(els[1], Element::Float(f64::INFINITY));
604        assert_eq!(els[2], Element::Float(f64::NEG_INFINITY));
605        let Element::Float(nan) = els[3] else { panic!("{:?}", els[3]) };
606        assert!(nan.is_nan());
607    }
608
609    /// The schema allows a family of spellings; all of them must read.
610    #[test]
611    fn inline_complex_accepts_every_spelling_the_schema_allows() {
612        let cases = [
613            ("0j", (0.0, 0.0)),
614            ("(1+2j)", (1.0, 2.0)),
615            ("1+2j", (1.0, 2.0)),
616            ("(1-2j)", (1.0, -2.0)),
617            ("-1j", (0.0, -1.0)),
618            ("(-0+0j)", (-0.0, 0.0)),
619            ("3", (3.0, 0.0)),
620            ("2i", (0.0, 2.0)),
621            ("(1.5e-3+2.5e+4j)", (1.5e-3, 2.5e4)),
622            // A bare sign before the unit means one.
623            ("(1+j)", (1.0, 1.0)),
624            ("(1-j)", (1.0, -1.0)),
625        ];
626        for (text, (re, im)) in cases {
627            let got = parse_inline_complex(text).unwrap_or_else(|e| panic!("{text}: {e}"));
628            assert_eq!(got.0, re, "real part of {text}");
629            assert_eq!(got.1, im, "imaginary part of {text}");
630        }
631
632        // The non-finites, which the corpus does carry.
633        let (re, im) = parse_inline_complex("(nan-infj)").unwrap();
634        assert!(re.is_nan());
635        assert_eq!(im, f64::NEG_INFINITY);
636    }
637
638    /// Everything we write must read back, which is the property that
639    /// matters for a round trip through inline form.
640    #[test]
641    fn complex_spellings_round_trip_through_the_parser() {
642        let values = [
643            (0.0, 0.0),
644            (-0.0, 0.0),
645            (1.0, 2.0),
646            (1.0, -2.0),
647            (0.0, -1.0),
648            (1.5e-3, 2.5e4),
649            (f64::MAX, f64::MIN_POSITIVE),
650        ];
651        for (re, im) in values {
652            let text = crate::core::pyrepr::repr_complex(re, im);
653            let (back_re, back_im) = parse_inline_complex(&text).unwrap();
654            assert_eq!(back_re.to_bits(), re.to_bits(), "{text}");
655            assert_eq!(back_im.to_bits(), im.to_bits(), "{text}");
656        }
657    }
658
659    #[test]
660    fn inline_compound_records_stay_grouped() {
661        let doc = parse_document(
662            "a:\n  data: [[1, 2.5], [3, 4.5]]\n  shape: [2]\n  datatype:\n  \
663             - {name: n, datatype: int32}\n  - {name: x, datatype: float64}\n",
664        )
665        .unwrap();
666        let root = doc.root().unwrap();
667        let nd = Ndarray::parse(&doc, doc.mapping_get(root, "a").unwrap()).unwrap();
668        let els = decode_inline(&doc, &nd, &[2]).unwrap();
669        assert_eq!(
670            els,
671            vec![
672                Element::Record(vec![Element::Int(1), Element::Float(2.5)]),
673                Element::Record(vec![Element::Int(3), Element::Float(4.5)]),
674            ]
675        );
676    }
677
678    #[test]
679    fn inline_data_must_match_the_declared_shape() {
680        let doc =
681            parse_document("a:\n  data: [1, 2, 3]\n  datatype: int32\n  shape: [4]\n").unwrap();
682        let root = doc.root().unwrap();
683        let nd = Ndarray::parse(&doc, doc.mapping_get(root, "a").unwrap()).unwrap();
684        let err = decode_inline(&doc, &nd, &[4]).unwrap_err();
685        assert!(err.message().contains("shape calls for 4"), "{err}");
686    }
687
688    /// A block-backed array decoded and re-read through inline form must
689    /// come back the same, which is the property `tree_inlined` relies on.
690    #[test]
691    fn a_block_array_survives_a_trip_through_inline_form() {
692        let nd =
693            ndarray("a:\n  source: 0\n  shape: [5]\n  datatype: float64\n  byteorder: little\n");
694        let values = [1.5f64, -2.25, 0.0, f64::MAX, -0.125];
695        let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
696        let original = decode_all(&nd, &[5], &bytes).unwrap();
697
698        let mut doc = parse_document(
699            "a:\n  source: 0\n  shape: [5]\n  datatype: float64\n  byteorder: little\n",
700        )
701        .unwrap();
702        let root = doc.root().unwrap();
703        let node = doc.mapping_get(root, "a").unwrap();
704        inline_ndarray(&mut doc, node, &original, &[5]).unwrap();
705
706        let inlined = Ndarray::parse(&doc, node).unwrap();
707        let read_back = decode_inline(&doc, &inlined, &[5]).unwrap();
708        assert_eq!(read_back, original);
709    }
710
711    #[test]
712    fn decodes_little_endian_integers() {
713        let nd = ndarray("a:\n  source: 0\n  shape: [4]\n  datatype: int32\n  byteorder: little\n");
714        let mut bytes = Vec::new();
715        for v in [1i32, -1, 256, i32::MIN] {
716            bytes.extend_from_slice(&v.to_le_bytes());
717        }
718        let els = decode_all(&nd, &[4], &bytes).unwrap();
719        assert_eq!(
720            els,
721            vec![
722                Element::Int(1),
723                Element::Int(-1),
724                Element::Int(256),
725                Element::Int(i64::from(i32::MIN)),
726            ]
727        );
728    }
729
730    #[test]
731    fn decodes_big_endian_integers() {
732        let nd = ndarray("a:\n  source: 0\n  shape: [3]\n  datatype: int16\n  byteorder: big\n");
733        let mut bytes = Vec::new();
734        for v in [1i16, -2, 1000] {
735            bytes.extend_from_slice(&v.to_be_bytes());
736        }
737        let els = decode_all(&nd, &[3], &bytes).unwrap();
738        assert_eq!(els, vec![Element::Int(1), Element::Int(-2), Element::Int(1000)]);
739    }
740
741    #[test]
742    fn byte_order_actually_changes_the_value() {
743        let bytes = [0x01u8, 0x00];
744        let le =
745            ndarray("a:\n  source: 0\n  shape: [1]\n  datatype: uint16\n  byteorder: little\n");
746        let be = ndarray("a:\n  source: 0\n  shape: [1]\n  datatype: uint16\n  byteorder: big\n");
747        assert_eq!(decode_all(&le, &[1], &bytes).unwrap(), vec![Element::Uint(1)]);
748        assert_eq!(decode_all(&be, &[1], &bytes).unwrap(), vec![Element::Uint(256)]);
749    }
750
751    #[test]
752    fn decodes_floats_of_every_width() {
753        let nd =
754            ndarray("a:\n  source: 0\n  shape: [2]\n  datatype: float64\n  byteorder: little\n");
755        let mut bytes = Vec::new();
756        bytes.extend_from_slice(&1.5f64.to_le_bytes());
757        bytes.extend_from_slice(&(-0.25f64).to_le_bytes());
758        assert_eq!(
759            decode_all(&nd, &[2], &bytes).unwrap(),
760            vec![Element::Float(1.5), Element::Float(-0.25)]
761        );
762
763        let nd =
764            ndarray("a:\n  source: 0\n  shape: [1]\n  datatype: float32\n  byteorder: little\n");
765        assert_eq!(
766            decode_all(&nd, &[1], &2.5f32.to_le_bytes()).unwrap(),
767            vec![Element::Float(2.5)]
768        );
769
770        let nd =
771            ndarray("a:\n  source: 0\n  shape: [1]\n  datatype: float16\n  byteorder: little\n");
772        let h = half::f16::from_f32(0.5);
773        assert_eq!(
774            decode_all(&nd, &[1], &h.to_bits().to_le_bytes()).unwrap(),
775            vec![Element::Float(0.5)]
776        );
777    }
778
779    #[test]
780    fn decodes_bools_and_text() {
781        let nd = ndarray("a:\n  source: 0\n  shape: [2]\n  datatype: bool8\n  byteorder: little\n");
782        assert_eq!(
783            decode_all(&nd, &[2], &[0u8, 1]).unwrap(),
784            vec![Element::Bool(false), Element::Bool(true)]
785        );
786
787        // Fixed-length ASCII is NUL-padded and trimmed on the way out.
788        let nd = ndarray(
789            "a:\n  source: 0\n  shape: [2]\n  datatype: ['ascii', 4]\n  byteorder: little\n",
790        );
791        let bytes = b"M31\0Cas\0";
792        assert_eq!(
793            decode_all(&nd, &[2], bytes).unwrap(),
794            vec![Element::Text("M31".into()), Element::Text("Cas".into())]
795        );
796    }
797
798    #[test]
799    fn decodes_ucs4_text() {
800        let nd = ndarray(
801            "a:\n  source: 0\n  shape: [1]\n  datatype: ['ucs4', 3]\n  byteorder: little\n",
802        );
803        let mut bytes = Vec::new();
804        for cp in ['a' as u32, 0x00E9 /* é */, 0] {
805            bytes.extend_from_slice(&cp.to_le_bytes());
806        }
807        assert_eq!(decode_all(&nd, &[1], &bytes).unwrap(), vec![Element::Text("aé".into())]);
808    }
809
810    #[test]
811    fn honours_offset() {
812        let nd = ndarray(
813            "a:\n  source: 0\n  shape: [2]\n  datatype: uint8\n  byteorder: little\n  offset: 3\n",
814        );
815        let bytes = [9u8, 9, 9, 1, 2];
816        assert_eq!(
817            decode_all(&nd, &[2], &bytes).unwrap(),
818            vec![Element::Uint(1), Element::Uint(2)]
819        );
820    }
821
822    #[test]
823    fn honours_strides_for_a_fortran_order_array() {
824        // A 2x3 array stored column-major: strides are [1, 2] elements.
825        let nd = ndarray(
826            "a:\n  source: 0\n  shape: [2, 3]\n  datatype: uint8\n  byteorder: little\n  \
827             strides: [1, 2]\n",
828        );
829        // Column-major layout of [[1,2,3],[4,5,6]].
830        let bytes = [1u8, 4, 2, 5, 3, 6];
831        let els = decode_all(&nd, &[2, 3], &bytes).unwrap();
832        let values: Vec<u64> = els
833            .iter()
834            .map(|e| match e {
835                Element::Uint(v) => *v,
836                _ => unreachable!(),
837            })
838            .collect();
839        // Read back in C order it must be the logical array.
840        assert_eq!(values, vec![1, 2, 3, 4, 5, 6]);
841    }
842
843    #[test]
844    fn honours_strides_for_a_tile_view() {
845        // A 2x2 tile of a 4x4 uint8 image, starting at row 1 column 1.
846        let nd = ndarray(
847            "a:\n  source: 0\n  shape: [2, 2]\n  datatype: uint8\n  byteorder: little\n  \
848             strides: [4, 1]\n  offset: 5\n",
849        );
850        let bytes: Vec<u8> = (0..16).collect();
851        let els = decode_all(&nd, &[2, 2], &bytes).unwrap();
852        let values: Vec<u64> = els
853            .iter()
854            .map(|e| match e {
855                Element::Uint(v) => *v,
856                _ => unreachable!(),
857            })
858            .collect();
859        assert_eq!(values, vec![5, 6, 9, 10]);
860    }
861
862    #[test]
863    fn decodes_compound_records() {
864        let nd = ndarray(
865            "a:\n  source: 0\n  shape: [2]\n  byteorder: little\n  \
866             datatype:\n    - name: id\n      datatype: uint16\n    \
867             - name: value\n      datatype: float32\n",
868        );
869        let mut bytes = Vec::new();
870        for (id, value) in [(1u16, 1.5f32), (2, -2.5)] {
871            bytes.extend_from_slice(&id.to_le_bytes());
872            bytes.extend_from_slice(&value.to_le_bytes());
873        }
874        let els = decode_all(&nd, &[2], &bytes).unwrap();
875        assert_eq!(
876            els,
877            vec![
878                Element::Record(vec![Element::Uint(1), Element::Float(1.5)]),
879                Element::Record(vec![Element::Uint(2), Element::Float(-2.5)]),
880            ]
881        );
882    }
883
884    #[test]
885    fn truncated_data_is_an_error_not_a_panic() {
886        let nd = ndarray("a:\n  source: 0\n  shape: [4]\n  datatype: int64\n  byteorder: little\n");
887        assert!(decode_all(&nd, &[4], &[0u8; 8]).is_err());
888    }
889
890    #[test]
891    fn nesting_reproduces_the_shape() {
892        let mut doc = Document::new();
893        let els: Vec<Element> = (0..6).map(Element::Uint).collect();
894        let node = nest(&mut doc, &els, &[2, 3]);
895        doc.set_root(node);
896
897        assert_eq!(doc.container_len(node), Some(2));
898        let first = doc.sequence_get(node, 0).unwrap();
899        assert_eq!(doc.container_len(first), Some(3));
900        assert_eq!(doc.resolved(doc.sequence_get(first, 2).unwrap()).as_str(), Some("2"));
901    }
902
903    #[test]
904    fn float_formatting_uses_yaml_spellings() {
905        assert_eq!(format_float(f64::NAN), ".nan");
906        assert_eq!(format_float(f64::INFINITY), ".inf");
907        assert_eq!(format_float(f64::NEG_INFINITY), "-.inf");
908        // A whole float keeps a decimal point so it does not read as an int.
909        assert_eq!(format_float(1.0), "1.0");
910        assert_eq!(format_float(1.5), "1.5");
911    }
912
913    #[test]
914    fn inlining_replaces_source_with_data() {
915        let mut doc = parse_document(
916            "a:\n  source: 0\n  shape: [4]\n  datatype: uint8\n  byteorder: little\n  offset: 0\n",
917        )
918        .unwrap();
919        let root = doc.root().unwrap();
920        let nd_id = doc.mapping_get(root, "a").unwrap();
921
922        let els: Vec<Element> = (0..4).map(Element::Uint).collect();
923        inline_ndarray(&mut doc, nd_id, &els, &[4]).unwrap();
924
925        assert!(doc.mapping_get(nd_id, "source").is_none(), "source must be removed");
926        assert!(doc.mapping_get(nd_id, "byteorder").is_none(), "byteorder is meaningless inline");
927        assert!(doc.mapping_get(nd_id, "offset").is_none(), "offset is meaningless inline");
928
929        let data = doc.mapping_get(nd_id, "data").expect("data must be added");
930        assert_eq!(doc.container_len(data), Some(4));
931        // The datatype survives; it still describes the values.
932        assert!(doc.mapping_get(nd_id, "datatype").is_some());
933    }
934}