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