Skip to main content

asdf/
lib.rs

1//! Read and write ASDF files from Rust.
2//!
3//! [ASDF](https://www.asdf-format.org/) (Advanced Scientific Data Format) is
4//! a hybrid format: a YAML tree describing the data, followed by binary
5//! blocks holding it. It is the native format of the Nancy Grace Roman Space
6//! Telescope and is widely used across astronomy.
7//!
8//! This is the idiomatic Rust face of the library. It borrows rather than
9//! copies wherever the format allows, returns [`Result`] rather than error
10//! codes, and needs no `unsafe`. For C interoperability use the `libasdf-rs`
11//! crate instead, which exposes the same engine through libasdf's C ABI.
12//!
13//! # Reading
14//!
15//! ```no_run
16//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
17//! use asdf::AsdfFile;
18//!
19//! let file = AsdfFile::open("observation.asdf")?;
20//! let tree = file.tree()?.expect("a tree");
21//!
22//! // Values are addressed by path.
23//! if let Some(name) = tree.get("meta/instrument/name").and_then(|v| v.as_str()) {
24//!     println!("instrument: {name}");
25//! }
26//!
27//! // An array reads back as whatever scalar type its values fit.
28//! let values: Vec<f64> = file.read_array_of("data")?;
29//! println!("{} elements", values.len());
30//! # Ok(())
31//! # }
32//! ```
33//!
34//! # Writing
35//!
36//! ```no_run
37//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
38//! use asdf::AsdfBuilder;
39//!
40//! let mut builder = AsdfBuilder::new();
41//! builder.set_str("name", "Dennis Richie")?;
42//! builder.set_i64("foo", 42)?;
43//!
44//! // An array's data goes in a binary block, referenced from the tree.
45//! let squares: Vec<u64> = (0..100).map(|i| i * i).collect();
46//! builder.set_array("powers/squares", &squares)?;
47//!
48//! builder.write_to_path("out.asdf")?;
49//! # Ok(())
50//! # }
51//! ```
52//!
53//! # Editing
54//!
55//! An existing file is changed through [`AsdfFile::edit`], which carries the
56//! tree and the blocks over so every `source: N` still points where it did.
57//!
58//! ```no_run
59//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
60//! use asdf::{AsdfFile, Compression};
61//!
62//! let file = AsdfFile::open("observation.asdf")?;
63//! let mut edited = file.edit()?;
64//! edited.set_str("meta/observer", "M. Curie")?;
65//! edited.recompress(Compression::Zlib).write_to_path("observation.asdf")?;
66//! # Ok(())
67//! # }
68//! ```
69
70#![forbid(unsafe_code)]
71#![warn(missing_docs)]
72
73// Named so `alloc::` paths can be written directly. The crate links `std`,
74// but spelling each item at the narrowest layer that defines it keeps a
75// future `no_std` build a small step away.
76extern crate alloc;
77
78use alloc::borrow::Cow;
79use std::path::Path;
80
81use asdf_core::core::elements::decode_all;
82use asdf_core::yaml::{
83    self, CompareOptions, Document, NodeData, NodeId, Resolved, ScalarStyle, Schema, Tag,
84};
85use asdf_core::{PendingBlock, Reader, Writer};
86
87pub use asdf_core::ChecksumStatus;
88// These name types that already appear in this crate's public signatures --
89// `as_ndarray` returns an `Ndarray`, `native_byte_order` a `ByteOrder`,
90// `scalar_datatype` a `Datatype` -- so without re-exporting them a caller
91// could not name what they were given without depending on `asdf-core`.
92pub use asdf_core::compression::Compression;
93pub use asdf_core::core::datatype::{ByteOrder, Datatype, Field, ScalarType};
94pub use asdf_core::core::elements::Element;
95pub use asdf_core::core::ndarray::{Mask, Ndarray, Source};
96pub use asdf_core::core::provenance::{ExtensionMetadata, History, HistoryEntry, Meta, Software};
97pub use asdf_core::core::time::{Civil, Location, Time, TimeFormat, TimeScale};
98pub use asdf_core::error::{Error, ErrorCode};
99pub use asdf_core::events::{Event, EventOptions, render_event};
100pub use asdf_core::info::InfoOptions;
101pub use asdf_core::version::Version;
102
103/// The result type used throughout this crate.
104pub type Result<T> = core::result::Result<T, Error>;
105
106/// A scalar type an array can be written from and read back as.
107///
108/// Implemented for the numeric types ASDF's `core/ndarray` schema names, so
109/// [`AsdfBuilder::set_array`] and [`AsdfFile::read_array_of`] work for any of
110/// them without a method per type.
111///
112/// Sealed: the set of scalar types is the schema's, not the caller's.
113pub trait ArrayElement: sealed::Sealed + Copy {
114    /// The schema's name for this type.
115    const SCALAR: ScalarType;
116
117    /// This value's bytes in the machine's own order.
118    fn to_bytes(self) -> Vec<u8>;
119
120    /// Read a decoded element as this type, or `None` if it is not one.
121    ///
122    /// Narrowing that would lose the value is a `None` rather than a silent
123    /// truncation: a caller asking for `Vec<i32>` wants the numbers, not
124    /// whatever survives the cast.
125    fn from_element(element: &Element) -> Option<Self>;
126
127    /// Decode a whole buffer that is already this exact type, in `order`.
128    ///
129    /// The general path decodes to [`Element`] first, which costs an
130    /// intermediate allocation several times the size of the data and a
131    /// branch per element. When the stored type already *is* `Self` and the
132    /// array is contiguous, none of that is needed -- and that is the common
133    /// case, because a writer stores what it had. On a little-endian host
134    /// reading little-endian data this is a load per element and vectorises
135    /// into roughly a `memcpy`.
136    #[doc(hidden)]
137    fn decode_native(bytes: &[u8], order: ByteOrder) -> Vec<Self>;
138
139    /// Encode a whole slice of this type into the machine's own order.
140    ///
141    /// The counterpart to [`decode_native`](ArrayElement::decode_native), and
142    /// it exists for the same reason: doing this an element at a time meant
143    /// `to_bytes` returning a fresh `Vec` per element, so writing a
144    /// four-million-element array made four million heap allocations.
145    #[doc(hidden)]
146    fn encode_native(values: &[Self]) -> Vec<u8>;
147}
148
149mod sealed {
150    pub trait Sealed {}
151}
152
153/// Implement [`ArrayElement`] for an integer type.
154macro_rules! integer_element {
155    ($ty:ty, $scalar:ident) => {
156        impl sealed::Sealed for $ty {}
157        impl ArrayElement for $ty {
158            const SCALAR: ScalarType = ScalarType::$scalar;
159
160            fn to_bytes(self) -> Vec<u8> {
161                self.to_ne_bytes().to_vec()
162            }
163
164            fn from_element(element: &Element) -> Option<Self> {
165                match element {
166                    Element::Int(v) => <$ty>::try_from(*v).ok(),
167                    Element::Uint(v) => <$ty>::try_from(*v).ok(),
168                    Element::Bool(v) => Some(<$ty>::from(*v)),
169                    _ => None,
170                }
171            }
172
173            fn decode_native(bytes: &[u8], order: ByteOrder) -> Vec<Self> {
174                let (chunks, _) = bytes.as_chunks::<{ size_of::<$ty>() }>();
175                match order {
176                    ByteOrder::Big => chunks.iter().map(|c| <$ty>::from_be_bytes(*c)).collect(),
177                    _ => chunks.iter().map(|c| <$ty>::from_le_bytes(*c)).collect(),
178                }
179            }
180
181            fn encode_native(values: &[Self]) -> Vec<u8> {
182                let mut out = Vec::with_capacity(values.len() * size_of::<$ty>());
183                for value in values {
184                    out.extend_from_slice(&value.to_ne_bytes());
185                }
186                out
187            }
188        }
189    };
190}
191
192integer_element!(i8, Int8);
193integer_element!(i16, Int16);
194integer_element!(i32, Int32);
195integer_element!(i64, Int64);
196integer_element!(u8, Uint8);
197integer_element!(u16, Uint16);
198integer_element!(u32, Uint32);
199integer_element!(u64, Uint64);
200
201/// Implement [`ArrayElement`] for a float type.
202macro_rules! float_element {
203    ($ty:ty, $scalar:ident) => {
204        impl sealed::Sealed for $ty {}
205        impl ArrayElement for $ty {
206            const SCALAR: ScalarType = ScalarType::$scalar;
207
208            fn to_bytes(self) -> Vec<u8> {
209                self.to_ne_bytes().to_vec()
210            }
211
212            fn from_element(element: &Element) -> Option<Self> {
213                match element {
214                    Element::Float(v) => Some(*v as $ty),
215                    // An integer converts only while it is exact; a
216                    // `u64` beyond a float's precision is not this value.
217                    Element::Int(v) => {
218                        let converted = *v as $ty;
219                        (converted as i64 == *v).then_some(converted)
220                    }
221                    Element::Uint(v) => {
222                        let converted = *v as $ty;
223                        (converted as u64 == *v).then_some(converted)
224                    }
225                    _ => None,
226                }
227            }
228
229            fn decode_native(bytes: &[u8], order: ByteOrder) -> Vec<Self> {
230                let (chunks, _) = bytes.as_chunks::<{ size_of::<$ty>() }>();
231                match order {
232                    ByteOrder::Big => chunks.iter().map(|c| <$ty>::from_be_bytes(*c)).collect(),
233                    _ => chunks.iter().map(|c| <$ty>::from_le_bytes(*c)).collect(),
234                }
235            }
236
237            fn encode_native(values: &[Self]) -> Vec<u8> {
238                let mut out = Vec::with_capacity(values.len() * size_of::<$ty>());
239                for value in values {
240                    out.extend_from_slice(&value.to_ne_bytes());
241                }
242                out
243            }
244        }
245    };
246}
247
248float_element!(f32, Float32);
249float_element!(f64, Float64);
250
251/// An ASDF file opened for reading.
252#[derive(Debug)]
253pub struct AsdfFile {
254    reader: Reader,
255}
256
257impl AsdfFile {
258    /// Open a file from disk.
259    ///
260    /// The file is memory-mapped, so a large array costs nothing until it is
261    /// actually read.
262    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
263        Ok(Self { reader: Reader::open(path)? })
264    }
265
266    /// Open a file already held in memory.
267    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
268        Ok(Self { reader: Reader::from_bytes(bytes)? })
269    }
270
271    /// The ASDF file-format version from the header line.
272    pub fn format_version(&self) -> &Version {
273        &self.reader.layout().format_version
274    }
275
276    /// The ASDF Standard version, if the file records one.
277    pub fn standard_version(&self) -> Option<&Version> {
278        self.reader.layout().standard_version.as_ref()
279    }
280
281    /// The YAML tree.
282    ///
283    /// A file in exploded form may legitimately have none, hence the
284    /// [`Option`].
285    pub fn tree(&self) -> Result<Option<Tree>> {
286        Ok(self.reader.tree()?.map(|document| Tree { document }))
287    }
288
289    /// The tree with every block-backed array replaced by inline data.
290    ///
291    /// This is the transformation the ASDF Standard's reference corpus
292    /// prescribes before comparing files. Arrays whose data lives outside
293    /// this file are left alone and named in the returned list.
294    pub fn tree_inlined(&self) -> Result<Option<(Tree, Vec<String>)>> {
295        Ok(self.reader.tree_inlined()?.map(|(document, skipped)| (Tree { document }, skipped)))
296    }
297
298    /// The number of binary blocks.
299    pub fn block_count(&self) -> usize {
300        self.reader.block_count()
301    }
302
303    /// A block's data, decompressed if it needs to be.
304    ///
305    /// An uncompressed block borrows directly from the mapped file.
306    pub fn block_data(&self, index: usize) -> Result<Cow<'_, [u8]>> {
307        self.reader.block_data(index)
308    }
309
310    /// A block's bytes exactly as stored, without decompressing.
311    pub fn block_raw(&self, index: usize) -> Result<&[u8]> {
312        self.reader.block_raw(index)
313    }
314
315    /// How a block is compressed.
316    pub fn block_compression(&self, index: usize) -> Result<Compression> {
317        self.reader.block_compression(index)
318    }
319
320    /// Verify a block's MD5 checksum.
321    ///
322    /// An absent checksum is reported as [`ChecksumStatus::Absent`] rather
323    /// than as a failure: the standard makes it optional.
324    pub fn verify_block(&self, index: usize) -> Result<ChecksumStatus> {
325        Ok(self.reader.verify_block_checksum(index)?.0)
326    }
327
328    /// Resolve an array's source to a block index in this file.
329    fn block_for(&self, array: &Ndarray) -> Result<usize> {
330        match &array.source {
331            Source::Block(index) => Ok(*index),
332            Source::LastBlock => self
333                .reader
334                .block_count()
335                .checked_sub(1)
336                .ok_or_else(|| Error::new(ErrorCode::InvalidArgument, "the file has no blocks")),
337            Source::External(uri) => Err(Error::new(
338                ErrorCode::InvalidArgument,
339                format!("array data lives in another file: {uri}"),
340            )),
341            Source::Inline(_) => {
342                Err(Error::new(ErrorCode::InvalidArgument, "array data is inline, not in a block"))
343            }
344        }
345    }
346
347    /// Read every element of a block-backed or external array.
348    ///
349    /// An array whose `source` names another file -- the standard's exploded
350    /// form -- is followed, provided this file was opened from a path and the
351    /// name resolves to a file beneath its directory.
352    ///
353    /// An array whose data is *inline* in the tree carries no block, so it is
354    /// an error here; read one with [`Tree::read_array`], which has the tree
355    /// the values live in.
356    pub fn read_array(&self, array: &Ndarray) -> Result<Vec<Element>> {
357        if let Source::External(uri) = &array.source {
358            let data = self.reader.external_block(uri)?;
359            let shape = array.resolved_shape(Some(data.len() as u64))?;
360            return decode_all(array, &shape, &data);
361        }
362        let index = self.block_for(array)?;
363        let data = self.block_data(index)?;
364        let shape = array.resolved_shape(Some(data.len() as u64))?;
365        decode_all(array, &shape, &data)
366    }
367
368    /// Read every element of the array at `path`, wherever its data lives.
369    ///
370    /// The one call that covers all four cases: a block in this file, the
371    /// last block, another file, or inline in the tree. It parses the tree
372    /// each time, so a loop over many arrays is better served by holding a
373    /// [`Tree`] and using [`Tree::read_array`] or [`AsdfFile::read_array`].
374    pub fn read_array_at(&self, path: &str) -> Result<Vec<Element>> {
375        let tree = self.tree()?.ok_or_else(|| {
376            Error::new(ErrorCode::InvalidArgument, "this file has no tree to look in")
377        })?;
378        let value = tree.get(path).ok_or_else(|| {
379            Error::new(ErrorCode::InvalidArgument, format!("no value at {path:?}"))
380        })?;
381        let array = value.as_ndarray().ok_or_else(|| {
382            Error::new(ErrorCode::InvalidArgument, format!("the value at {path:?} is not an array"))
383        })?;
384        match array.source {
385            Source::Inline(_) => tree.read_array(&array),
386            _ => self.read_array(&array),
387        }
388    }
389
390    /// Read an array converted to `f64`.
391    ///
392    /// Every numeric type converts; a string or compound array does not.
393    pub fn read_array_f64(&self, array: &Ndarray) -> Result<Vec<f64>> {
394        as_f64(self.read_array(array)?)
395    }
396
397    /// Read an array converted to `i64`.
398    ///
399    /// A float with a fractional part is an error rather than being
400    /// truncated silently.
401    pub fn read_array_i64(&self, array: &Ndarray) -> Result<Vec<i64>> {
402        as_i64(self.read_array(array)?)
403    }
404
405    /// [`AsdfFile::read_array_at`] converted to `f64`.
406    pub fn read_array_f64_at(&self, path: &str) -> Result<Vec<f64>> {
407        as_f64(self.read_array_at(path)?)
408    }
409
410    /// [`AsdfFile::read_array_at`] converted to `i64`.
411    pub fn read_array_i64_at(&self, path: &str) -> Result<Vec<i64>> {
412        as_i64(self.read_array_at(path)?)
413    }
414
415    /// Read an array as a `Vec` of any scalar type.
416    ///
417    /// A value that will not fit the requested type is an error rather than
418    /// a silent truncation: a caller asking for `Vec<i32>` wants the numbers
419    /// the file holds, not whatever survives the cast.
420    ///
421    /// ```no_run
422    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
423    /// let file = asdf::AsdfFile::open("observation.asdf")?;
424    /// let counts: Vec<u16> = file.read_array_of("data")?;
425    /// # Ok(())
426    /// # }
427    /// ```
428    pub fn read_array_of<T: ArrayElement>(&self, path: &str) -> Result<Vec<T>> {
429        let tree = self.tree()?.ok_or_else(|| {
430            Error::new(ErrorCode::InvalidArgument, "this file has no tree to look in")
431        })?;
432        let value = tree.get(path).ok_or_else(|| {
433            Error::new(ErrorCode::InvalidArgument, format!("no value at {path:?}"))
434        })?;
435        let array = value.as_ndarray().ok_or_else(|| {
436            Error::new(ErrorCode::InvalidArgument, format!("the value at {path:?} is not an array"))
437        })?;
438
439        // Take the bulk path when the stored elements already are `T` laid
440        // out end to end. Anything else -- a different width, a compound
441        // type, custom strides, an offset into the block -- goes the general
442        // way, which handles every case and is what correctness is judged on.
443        if let Some(bytes) = self.contiguous_bytes_of(&array)?
444            && bulk_readable::<T>(&array)
445        {
446            return Ok(T::decode_native(&bytes, element_order(&array)));
447        }
448
449        match array.source {
450            Source::Inline(_) => as_type(tree.read_array(&array)?),
451            _ => as_type(self.read_array(&array)?),
452        }
453    }
454
455    /// The array's bytes, when they are one contiguous run this file owns.
456    ///
457    /// `None` for an inline array, whose elements live in the tree rather
458    /// than in a block.
459    fn contiguous_bytes_of(&self, array: &Ndarray) -> Result<Option<Cow<'_, [u8]>>> {
460        match &array.source {
461            Source::Inline(_) => Ok(None),
462            Source::External(uri) => Ok(Some(Cow::Owned(self.reader.external_block(uri)?))),
463            _ => {
464                let index = self.block_for(array)?;
465                Ok(Some(self.block_data(index)?))
466            }
467        }
468    }
469
470    /// A builder holding this file's tree and blocks, for editing.
471    ///
472    /// This is how a file is changed and written back: open it, edit the
473    /// builder, write it out. The blocks are carried over decompressed and
474    /// with their block indices intact, so every `source: N` in the tree
475    /// still points where it did.
476    ///
477    /// ```no_run
478    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
479    /// use asdf::AsdfFile;
480    ///
481    /// let file = AsdfFile::open("observation.asdf")?;
482    /// let mut edited = file.edit()?;
483    /// edited.set_str("meta/observer", "M. Curie")?;
484    /// edited.write_to_path("observation.asdf")?;
485    /// # Ok(())
486    /// # }
487    /// ```
488    pub fn edit(&self) -> Result<AsdfBuilder> {
489        let document = self.reader.tree()?.unwrap_or_else(Document::new_asdf);
490
491        // Each block's data comes across decompressed and is recompressed on
492        // the way out, so a builder that changes the compression setting
493        // applies it to what was already there as well as to what it adds.
494        let mut blocks = Vec::with_capacity(self.reader.block_count());
495        for index in 0..self.reader.block_count() {
496            let compression = self.reader.block_compression(index)?;
497            let data = self.reader.block_data(index)?.into_owned();
498            blocks.push(PendingBlock::compressed(data, compression));
499        }
500
501        Ok(AsdfBuilder { document, blocks, compression: Compression::None })
502    }
503
504    /// Render the file the way `asdf info` does.
505    ///
506    /// The rendering is what the command-line tool prints, so it is a
507    /// human-readable summary rather than anything to parse.
508    pub fn info(&self, options: InfoOptions) -> Result<String> {
509        asdf_core::info::render(&self.reader, options)
510    }
511
512    /// The low-level event stream: what the file contains, in order.
513    ///
514    /// Rather than building a tree, this reports what is there -- the version
515    /// headers, any comments, the block index, the tree's extent and
516    /// optionally the YAML events inside it, then each block. It is what
517    /// `asdf events` prints, and what a tool inspecting a damaged file wants,
518    /// since a tree that will not parse still yields everything around it.
519    pub fn events(&self, options: EventOptions) -> Vec<Event> {
520        asdf_core::events::events_from(self.reader.bytes(), self.reader.layout(), options)
521    }
522}
523
524/// Convert decoded elements to a requested scalar type.
525fn as_type<T: ArrayElement>(elements: Vec<Element>) -> Result<Vec<T>> {
526    elements
527        .into_iter()
528        .map(|element| {
529            T::from_element(&element).ok_or_else(|| {
530                Error::new(
531                    ErrorCode::InvalidArgument,
532                    format!("{element:?} does not fit {}", T::SCALAR.name()),
533                )
534            })
535        })
536        .collect()
537}
538
539/// The byte order an array's elements are stored in.
540///
541/// The datatype's own order wins where it sets one, as the schema says; the
542/// array's order is the default for its elements.
543fn element_order(array: &Ndarray) -> ByteOrder {
544    match array.datatype.byteorder {
545        ByteOrder::Big | ByteOrder::Little => array.datatype.byteorder,
546        _ => array.byteorder,
547    }
548}
549
550/// Whether `array` is a plain contiguous run of `T`.
551///
552/// Every condition here is one the bulk path cannot honour:
553///
554/// - a different scalar type, or a compound one, needs real conversion;
555/// - a `size` disagreeing with `T` means the stored width is not `T`'s;
556/// - explicit strides mean the elements are not end to end;
557/// - a non-zero offset means the run does not start where the block does;
558/// - a mask marks missing values, which a raw copy would silently keep.
559fn bulk_readable<T: ArrayElement>(array: &Ndarray) -> bool {
560    array.datatype.scalar == T::SCALAR
561        && array.datatype.size == size_of::<T>() as u64
562        && array.datatype.fields.is_empty()
563        && array.datatype.shape.is_empty()
564        && array.strides.is_none()
565        && array.offset == 0
566        && array.mask.is_none()
567}
568
569/// Convert decoded elements to `f64`.
570fn as_f64(elements: Vec<Element>) -> Result<Vec<f64>> {
571    elements
572        .into_iter()
573        .map(|element| match element {
574            Element::Float(v) => Ok(v),
575            Element::Int(v) => Ok(v as f64),
576            Element::Uint(v) => Ok(v as f64),
577            Element::Bool(v) => Ok(if v { 1.0 } else { 0.0 }),
578            other => Err(Error::new(
579                ErrorCode::InvalidArgument,
580                format!("{other:?} cannot be read as a number"),
581            )),
582        })
583        .collect()
584}
585
586/// Convert decoded elements to `i64`.
587fn as_i64(elements: Vec<Element>) -> Result<Vec<i64>> {
588    elements
589        .into_iter()
590        .map(|element| match element {
591            Element::Int(v) => Ok(v),
592            Element::Uint(v) => i64::try_from(v).map_err(|_| {
593                Error::new(ErrorCode::InvalidArgument, format!("{v} does not fit an i64"))
594            }),
595            Element::Bool(v) => Ok(i64::from(v)),
596            Element::Float(v) if v.fract() == 0.0 => Ok(v as i64),
597            other => Err(Error::new(
598                ErrorCode::InvalidArgument,
599                format!("{other:?} cannot be read as an integer"),
600            )),
601        })
602        .collect()
603}
604
605/// A parsed ASDF tree.
606#[derive(Clone, Debug)]
607pub struct Tree {
608    document: Document,
609}
610
611impl Tree {
612    /// The root value.
613    pub fn root(&self) -> Option<Value<'_>> {
614        self.document.root().map(|node| Value { document: &self.document, node })
615    }
616
617    /// The value at a path, using ASDF's YAML Pointer syntax.
618    ///
619    /// A numeric component indexes a sequence or names a mapping key
620    /// depending on what its parent is; negative indices count from the end.
621    pub fn get(&self, path: &str) -> Option<Value<'_>> {
622        self.document.lookup_str(path).map(|node| Value { document: &self.document, node })
623    }
624
625    /// Read every element of an array whose data is inline in this tree.
626    ///
627    /// Inline data needs no file: the values are already here. An array
628    /// backed by a block is read through [`AsdfFile::read_array`] instead,
629    /// and is an error here.
630    pub fn read_array(&self, array: &Ndarray) -> Result<Vec<Element>> {
631        let shape = array.resolved_shape(None)?;
632        asdf_core::core::decode_inline(&self.document, array, &shape)
633    }
634
635    /// What this file says about itself: what wrote it, and its history.
636    ///
637    /// Everything in it is optional, so a file that says nothing yields an
638    /// empty [`Meta`] rather than an error.
639    pub fn meta(&self) -> Result<Meta> {
640        let root = self
641            .document
642            .root()
643            .ok_or_else(|| Error::new(ErrorCode::InvalidArgument, "the tree has no root"))?;
644        Meta::parse(&self.document, root)
645    }
646
647    /// The underlying document, for callers needing the lower-level model.
648    pub fn document(&self) -> &Document {
649        &self.document
650    }
651
652    /// Whether two trees represent the same values.
653    ///
654    /// Presentation -- flow versus block, quoting, integer width -- is
655    /// ignored; tags, values, sequence order and the set of keys are not.
656    pub fn value_eq(&self, other: &Tree) -> bool {
657        yaml::compare(&self.document, &other.document, CompareOptions::default()).is_equal()
658    }
659
660    /// Render the tree back to YAML.
661    pub fn to_yaml(&self) -> Result<String> {
662        yaml::emit(&self.document)
663            .map_err(|e| Error::new(ErrorCode::YamlParseFailed, e.to_string()))
664    }
665}
666
667/// One value in a tree.
668#[derive(Clone, Copy, Debug)]
669pub struct Value<'a> {
670    document: &'a Document,
671    node: NodeId,
672}
673
674impl<'a> Value<'a> {
675    /// The value's YAML tag, which in ASDF is what gives it its type.
676    pub fn tag(&self) -> Option<&'a Tag> {
677        self.document.tag_of(self.node)
678    }
679
680    /// Whether the tag names this ASDF schema, ignoring its version.
681    ///
682    /// So `has_tag("core/ndarray")` matches both `core/ndarray-1.0.0` and
683    /// `core/ndarray-1.1.0`.
684    pub fn has_tag(&self, name: &str) -> bool {
685        self.tag().is_some_and(|t| t.split_version().0 == name)
686    }
687
688    /// The raw scalar text, whatever its type.
689    pub fn as_raw_str(&self) -> Option<&'a str> {
690        self.document.resolved(self.node).as_str()
691    }
692
693    /// The value as a string, if it is one.
694    ///
695    /// A quoted `"42"` is a string; an unquoted `42` is not.
696    pub fn as_str(&self) -> Option<&'a str> {
697        let node = self.document.resolved(self.node);
698        let NodeData::Scalar { value, style } = &node.data else {
699            return None;
700        };
701        matches!(yaml::resolve(value, *style, Schema::Libasdf), Resolved::String)
702            .then_some(value.as_str())
703    }
704
705    /// The value as a signed integer.
706    pub fn as_i64(&self) -> Option<i64> {
707        match self.resolved()? {
708            Resolved::Int(v, _) => Some(v),
709            Resolved::Uint(v, _) => i64::try_from(v).ok(),
710            _ => None,
711        }
712    }
713
714    /// The value as an unsigned integer.
715    pub fn as_u64(&self) -> Option<u64> {
716        match self.resolved()? {
717            Resolved::Uint(v, _) => Some(v),
718            Resolved::Int(v, _) => u64::try_from(v).ok(),
719            _ => None,
720        }
721    }
722
723    /// The value as a float. Integers convert.
724    pub fn as_f64(&self) -> Option<f64> {
725        match self.resolved()? {
726            Resolved::Double(v) => Some(v),
727            Resolved::Int(v, _) => Some(v as f64),
728            Resolved::Uint(v, _) => Some(v as f64),
729            _ => None,
730        }
731    }
732
733    /// The value as a boolean.
734    pub fn as_bool(&self) -> Option<bool> {
735        match self.resolved()? {
736            Resolved::Bool(v) => Some(v),
737            _ => None,
738        }
739    }
740
741    /// Whether the value is null.
742    pub fn is_null(&self) -> bool {
743        matches!(self.resolved(), Some(Resolved::Null))
744    }
745
746    fn resolved(&self) -> Option<Resolved> {
747        let node = self.document.resolved(self.node);
748        let NodeData::Scalar { value, style } = &node.data else {
749            return None;
750        };
751        Some(yaml::resolve(value, *style, Schema::Libasdf))
752    }
753
754    /// Whether this is a mapping.
755    pub fn is_mapping(&self) -> bool {
756        self.document.resolved(self.node).is_mapping()
757    }
758
759    /// Whether this is a sequence.
760    pub fn is_sequence(&self) -> bool {
761        self.document.resolved(self.node).is_sequence()
762    }
763
764    /// The number of children, for a mapping or sequence.
765    pub fn len(&self) -> Option<usize> {
766        self.document.container_len(self.node)
767    }
768
769    /// Whether this container has no children.
770    pub fn is_empty(&self) -> Option<bool> {
771        self.len().map(|n| n == 0)
772    }
773
774    /// A mapping entry by key.
775    pub fn get(&self, key: &str) -> Option<Value<'a>> {
776        self.document
777            .mapping_get(self.node, key)
778            .map(|node| Value { document: self.document, node })
779    }
780
781    /// A sequence element, with negative indices counting from the end.
782    pub fn at(&self, index: i64) -> Option<Value<'a>> {
783        self.document
784            .sequence_get(self.node, index)
785            .map(|node| Value { document: self.document, node })
786    }
787
788    /// A value further down, by path.
789    pub fn path(&self, path: &str) -> Option<Value<'a>> {
790        let parsed = yaml::Path::parse(path).ok()?;
791        self.document
792            .lookup_from(self.node, &parsed)
793            .map(|node| Value { document: self.document, node })
794    }
795
796    /// Iterate a mapping's entries in document order.
797    pub fn entries(&self) -> impl Iterator<Item = (&'a str, Value<'a>)> + 'a {
798        let document = self.document;
799        let entries = document.mapping_entries(self.node).unwrap_or(&[]);
800        entries.iter().map(move |entry| {
801            let key = document.resolved(entry.key).as_str().unwrap_or_default();
802            (key, Value { document, node: entry.value })
803        })
804    }
805
806    /// Iterate a sequence's items.
807    pub fn items(&self) -> impl Iterator<Item = Value<'a>> + 'a {
808        let document = self.document;
809        let items = document.sequence_items(self.node).unwrap_or(&[]);
810        items.iter().map(move |node| Value { document, node: *node })
811    }
812
813    /// Interpret this value as an ndarray.
814    ///
815    /// Returns `None` when it is not one; the array's data is then read
816    /// through [`AsdfFile::read_array`].
817    pub fn as_ndarray(&self) -> Option<Ndarray> {
818        Ndarray::parse(self.document, self.node).ok()
819    }
820
821    /// Interpret this value as a `core/software` record.
822    pub fn as_software(&self) -> Option<Software> {
823        Software::parse(self.document, self.node).ok()
824    }
825
826    /// Interpret this value as a `core/history_entry` record.
827    pub fn as_history_entry(&self) -> Option<HistoryEntry> {
828        HistoryEntry::parse(self.document, self.node).ok()
829    }
830
831    /// Interpret this value as a `core/extension_metadata` record.
832    pub fn as_extension_metadata(&self) -> Option<ExtensionMetadata> {
833        ExtensionMetadata::parse(self.document, self.node).ok()
834    }
835
836    /// Interpret this value as a `time/time`.
837    ///
838    /// The schema allows the whole value to be the time string, or a mapping
839    /// with `value`, `format`, `scale` and a location; both read here, and
840    /// the calendar breakdown comes with it where the value allows one.
841    pub fn as_time(&self) -> Option<Time> {
842        Time::parse(self.document, self.node).ok()
843    }
844
845    /// Whether this value is an alias to another node.
846    pub fn is_alias(&self) -> bool {
847        self.document.node(self.node).is_alias()
848    }
849}
850
851/// Builds an ASDF file.
852#[derive(Debug)]
853pub struct AsdfBuilder {
854    document: Document,
855    blocks: Vec<PendingBlock>,
856    compression: Compression,
857}
858
859impl Default for AsdfBuilder {
860    fn default() -> Self {
861        Self::new()
862    }
863}
864
865impl AsdfBuilder {
866    /// A builder for a new, empty file.
867    pub fn new() -> Self {
868        let mut document = Document::new_asdf();
869        let root = document.add(yaml::Node::mapping());
870        document.node_mut(root).tag = Some(Tag::parse("tag:stsci.edu:asdf/core/asdf-1.1.0"));
871        document.set_root(root);
872        Self { document, blocks: Vec::new(), compression: Compression::None }
873    }
874
875    /// Compress every array written from here on.
876    ///
877    /// Blocks already in the builder -- those an [`AsdfFile::edit`] brought
878    /// over -- keep the compression they had. Use
879    /// [`AsdfBuilder::recompress`] to change those too.
880    pub fn with_compression(mut self, compression: Compression) -> Self {
881        self.compression = compression;
882        self
883    }
884
885    /// Compress every block, including those already here.
886    ///
887    /// This is how a whole file's compression is changed: open it, edit it,
888    /// recompress, write it back. Each block's data is already decompressed
889    /// in the builder, so this only decides how it goes out.
890    pub fn recompress(mut self, compression: Compression) -> Self {
891        self.compression = compression;
892        for block in &mut self.blocks {
893            block.compression = compression;
894        }
895        self
896    }
897
898    /// The tree being built, for direct manipulation.
899    pub fn document_mut(&mut self) -> &mut Document {
900        &mut self.document
901    }
902
903    fn insert(&mut self, path: &str, node: NodeId) -> Result<()> {
904        self.document
905            .insert_at_str(path, node)
906            .map(|_| ())
907            .map_err(|e| Error::new(ErrorCode::InvalidArgument, e.to_string()))
908    }
909
910    /// Set a string. It is quoted where needed so it reads back as a string.
911    pub fn set_str(&mut self, path: &str, value: &str) -> Result<()> {
912        let style = match yaml::resolve(value, ScalarStyle::Plain, Schema::Libasdf) {
913            Resolved::String => ScalarStyle::Plain,
914            _ => ScalarStyle::SingleQuoted,
915        };
916        let node = self.document.add_scalar_styled(value, style);
917        self.insert(path, node)
918    }
919
920    /// Set a signed integer.
921    pub fn set_i64(&mut self, path: &str, value: i64) -> Result<()> {
922        let node = self.document.add_scalar(value.to_string());
923        self.insert(path, node)
924    }
925
926    /// Set an unsigned integer.
927    pub fn set_u64(&mut self, path: &str, value: u64) -> Result<()> {
928        let node = self.document.add_scalar(value.to_string());
929        self.insert(path, node)
930    }
931
932    /// Set a float.
933    pub fn set_f64(&mut self, path: &str, value: f64) -> Result<()> {
934        let node = self.document.add_scalar(asdf_core::core::elements::format_float(value));
935        self.insert(path, node)
936    }
937
938    /// Set a boolean.
939    pub fn set_bool(&mut self, path: &str, value: bool) -> Result<()> {
940        let node = self.document.add_scalar(if value { "true" } else { "false" });
941        self.insert(path, node)
942    }
943
944    /// Set a null.
945    pub fn set_null(&mut self, path: &str) -> Result<()> {
946        let node = self.document.add_scalar("null");
947        self.insert(path, node)
948    }
949
950    /// Write an array into a binary block and reference it from the tree.
951    fn set_array_bytes(
952        &mut self,
953        path: &str,
954        bytes: Vec<u8>,
955        shape: &[u64],
956        scalar: ScalarType,
957    ) -> Result<()> {
958        let index = self.blocks.len();
959        self.blocks.push(PendingBlock::compressed(bytes, self.compression));
960
961        // Build the core/ndarray mapping the schema defines.
962        let source = self.document.add_scalar(index.to_string());
963        let datatype = self.document.add_scalar(scalar.name());
964        let byteorder = self.document.add_scalar(ByteOrder::native().name());
965
966        let dims: Vec<NodeId> =
967            shape.iter().map(|d| self.document.add_scalar(d.to_string())).collect();
968        let shape_node = self.document.add_sequence(dims);
969        if let NodeData::Sequence { style, .. } = &mut self.document.node_mut(shape_node).data {
970            *style = yaml::CollectionStyle::Flow;
971        }
972
973        let keys: Vec<NodeId> = ["source", "datatype", "byteorder", "shape"]
974            .iter()
975            .map(|k| self.document.add_scalar(*k))
976            .collect();
977        let array = self.document.add_mapping(vec![
978            (keys[0], source),
979            (keys[1], datatype),
980            (keys[2], byteorder),
981            (keys[3], shape_node),
982        ]);
983        self.document.node_mut(array).tag =
984            Some(Tag::parse("tag:stsci.edu:asdf/core/ndarray-1.1.0"));
985
986        self.insert(path, array)
987    }
988
989    /// Write a one-dimensional array of any scalar type.
990    ///
991    /// ```
992    /// # fn main() -> Result<(), asdf::Error> {
993    /// let mut builder = asdf::AsdfBuilder::new();
994    /// builder.set_array("counts", &[1u16, 2, 3])?;
995    /// builder.set_array("ratios", &[0.5f32, 1.5])?;
996    /// # Ok(())
997    /// # }
998    /// ```
999    pub fn set_array<T: ArrayElement>(&mut self, path: &str, values: &[T]) -> Result<()> {
1000        self.set_array_shaped(path, values, &[values.len() as u64])
1001    }
1002
1003    /// Write a multi-dimensional array of any scalar type.
1004    ///
1005    /// The data is taken in C order, and its length must match the shape.
1006    pub fn set_array_shaped<T: ArrayElement>(
1007        &mut self,
1008        path: &str,
1009        values: &[T],
1010        shape: &[u64],
1011    ) -> Result<()> {
1012        let expected =
1013            shape.iter().try_fold(1u64, |acc, d| acc.checked_mul(*d)).ok_or_else(|| {
1014                Error::new(
1015                    ErrorCode::OverLimit,
1016                    format!("shape {shape:?} has more elements than 64 bits hold"),
1017                )
1018            })?;
1019        if expected != values.len() as u64 {
1020            return Err(Error::new(
1021                ErrorCode::InvalidArgument,
1022                format!("shape {shape:?} needs {expected} values, got {}", values.len()),
1023            ));
1024        }
1025        let bytes = T::encode_native(values);
1026        self.set_array_bytes(path, bytes, shape, T::SCALAR)
1027    }
1028
1029    /// Write a one-dimensional `u64` array. See [`AsdfBuilder::set_array`].
1030    pub fn set_array_u64(&mut self, path: &str, values: &[u64]) -> Result<()> {
1031        self.set_array(path, values)
1032    }
1033
1034    /// Write a one-dimensional `i64` array. See [`AsdfBuilder::set_array`].
1035    pub fn set_array_i64(&mut self, path: &str, values: &[i64]) -> Result<()> {
1036        self.set_array(path, values)
1037    }
1038
1039    /// Write a one-dimensional `f64` array. See [`AsdfBuilder::set_array`].
1040    pub fn set_array_f64(&mut self, path: &str, values: &[f64]) -> Result<()> {
1041        self.set_array(path, values)
1042    }
1043
1044    /// Write a multi-dimensional `f64` array.
1045    ///
1046    /// See [`AsdfBuilder::set_array_shaped`].
1047    pub fn set_array_f64_shaped(
1048        &mut self,
1049        path: &str,
1050        values: &[f64],
1051        shape: &[u64],
1052    ) -> Result<()> {
1053        self.set_array_shaped(path, values, shape)
1054    }
1055
1056    /// Add a raw binary block, returning its index.
1057    pub fn add_block(&mut self, data: Vec<u8>) -> usize {
1058        self.blocks.push(PendingBlock::compressed(data, self.compression));
1059        self.blocks.len() - 1
1060    }
1061
1062    fn writer(&self) -> Writer {
1063        let mut writer = Writer::from_document(self.document.clone());
1064        for block in &self.blocks {
1065            writer.add_block(block.clone());
1066        }
1067        writer
1068    }
1069
1070    /// Assemble the file in memory.
1071    pub fn to_bytes(&self) -> Result<Vec<u8>> {
1072        self.writer().to_bytes()
1073    }
1074
1075    /// Write the file to a path.
1076    pub fn write_to_path(&self, path: impl AsRef<Path>) -> Result<()> {
1077        self.writer().write_to_path(path)
1078    }
1079
1080    /// Write the file to a stream.
1081    pub fn write_to(&self, sink: &mut impl std::io::Write) -> Result<()> {
1082        self.writer().write_to(sink)
1083    }
1084}
1085
1086/// The default datatype for an array element, matching this machine.
1087pub fn native_byte_order() -> ByteOrder {
1088    ByteOrder::native()
1089}
1090
1091/// A datatype for one of the scalar types.
1092pub fn scalar_datatype(scalar: ScalarType) -> Datatype {
1093    Datatype::scalar(scalar)
1094}
1095
1096#[cfg(test)]
1097mod tests {
1098    use super::*;
1099
1100    fn round_trip(builder: &AsdfBuilder) -> AsdfFile {
1101        AsdfFile::from_bytes(builder.to_bytes().unwrap()).unwrap()
1102    }
1103
1104    #[test]
1105    fn an_inline_array_is_read_from_the_tree() {
1106        // Inline data needs no block, so it reads without a file behind it.
1107        let bytes = b"#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n\
1108%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n\
1109grid: !core/ndarray-1.1.0\n  data: [[1, 2, 3], [4, 5, 6]]\n  datatype: int32\n  shape: [2, 3]\n\
1110...\n"
1111            .to_vec();
1112        let file = AsdfFile::from_bytes(bytes).unwrap();
1113        let tree = file.tree().unwrap().unwrap();
1114        let array = tree.get("grid").unwrap().as_ndarray().unwrap();
1115
1116        let elements = tree.read_array(&array).unwrap();
1117        assert_eq!(elements, (1..=6).map(Element::Int).collect::<Vec<_>>());
1118
1119        // Through the file it is an error, since there is no block to read.
1120        assert!(file.read_array(&array).is_err());
1121
1122        // `read_array_at` dispatches for the caller.
1123        assert_eq!(file.read_array_at("grid").unwrap().len(), 6);
1124    }
1125
1126    #[test]
1127    fn read_array_at_covers_a_block_backed_array() {
1128        let values: Vec<i64> = vec![3, 1, 4, 1, 5];
1129        let mut builder = AsdfBuilder::new();
1130        builder.set_array_i64("data", &values).unwrap();
1131        let file = round_trip(&builder);
1132
1133        assert_eq!(
1134            file.read_array_at("data").unwrap(),
1135            values.iter().map(|v| Element::Int(*v)).collect::<Vec<_>>()
1136        );
1137        assert!(file.read_array_at("missing").is_err());
1138    }
1139
1140    #[test]
1141    fn an_external_array_is_followed_to_the_neighbouring_file() {
1142        let dir = std::env::temp_dir().join(format!("asdf-api-exploded-{}", std::process::id()));
1143        std::fs::create_dir_all(&dir).unwrap();
1144
1145        // The data file, written with our own builder.
1146        let values: Vec<i64> = vec![10, 20, 30, 40];
1147        let mut holder = AsdfBuilder::new();
1148        holder.set_array_i64("data", &values).unwrap();
1149        holder.write_to_path(dir.join("split0000.asdf")).unwrap();
1150
1151        // The referring file, whose array names it.
1152        let referring = format!(
1153            "#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n\
1154%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n\
1155data: !core/ndarray-1.1.0\n  source: split0000.asdf\n  datatype: int64\n  \
1156byteorder: little\n  shape: [{}]\n...\n",
1157            values.len()
1158        );
1159        let path = dir.join("split.asdf");
1160        std::fs::write(&path, referring).unwrap();
1161
1162        let file = AsdfFile::open(&path).unwrap();
1163        assert_eq!(file.block_count(), 0, "the referring file has no blocks of its own");
1164        assert_eq!(file.read_array_i64_at("data").unwrap(), values);
1165
1166        std::fs::remove_dir_all(&dir).ok();
1167    }
1168
1169    #[test]
1170    fn an_external_array_read_from_memory_is_refused() {
1171        // A file held in memory has no directory to resolve the name
1172        // against, so following it would mean guessing at the working
1173        // directory. The error says so rather than reporting "not found".
1174        let bytes = b"#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n\
1175%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n\
1176data: !core/ndarray-1.1.0\n  source: elsewhere.asdf\n  datatype: int64\n  shape: [2]\n\
1177...\n"
1178            .to_vec();
1179        let file = AsdfFile::from_bytes(bytes).unwrap();
1180        let err = file.read_array_at("data").unwrap_err();
1181        assert!(err.message().contains("not read from disk"), "{}", err.message());
1182    }
1183
1184    #[test]
1185    fn an_external_array_may_not_escape_its_directory() {
1186        let dir = std::env::temp_dir().join(format!("asdf-api-escape-{}", std::process::id()));
1187        std::fs::create_dir_all(&dir).unwrap();
1188        let path = dir.join("nosy.asdf");
1189        std::fs::write(
1190            &path,
1191            "#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n\
1192%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n\
1193data: !core/ndarray-1.1.0\n  source: ../../../etc/passwd\n  datatype: int64\n  shape: [2]\n\
1194...\n",
1195        )
1196        .unwrap();
1197
1198        let file = AsdfFile::open(&path).unwrap();
1199        let err = file.read_array_at("data").unwrap_err();
1200        assert!(err.message().contains("climbs out"), "{}", err.message());
1201
1202        std::fs::remove_dir_all(&dir).ok();
1203    }
1204
1205    /// Every scalar type round-trips through a file, at its own width.
1206    #[test]
1207    fn arrays_of_every_scalar_type_round_trip() {
1208        macro_rules! round_trip {
1209            ($ty:ty, $values:expr) => {{
1210                let values: Vec<$ty> = $values;
1211                let mut builder = AsdfBuilder::new();
1212                builder.set_array("data", &values).unwrap();
1213                let file = round_trip(&builder);
1214
1215                // The block holds exactly the elements, at the type's width.
1216                assert_eq!(
1217                    file.block_data(0).unwrap().len(),
1218                    values.len() * core::mem::size_of::<$ty>(),
1219                    "{}",
1220                    <$ty as ArrayElement>::SCALAR.name()
1221                );
1222
1223                let back: Vec<$ty> = file.read_array_of("data").unwrap();
1224                assert_eq!(back, values, "{}", <$ty as ArrayElement>::SCALAR.name());
1225            }};
1226        }
1227
1228        round_trip!(i8, vec![i8::MIN, -1, 0, 1, i8::MAX]);
1229        round_trip!(i16, vec![i16::MIN, -1, 0, i16::MAX]);
1230        round_trip!(i32, vec![i32::MIN, -1, 0, i32::MAX]);
1231        round_trip!(i64, vec![i64::MIN, -1, 0, i64::MAX]);
1232        round_trip!(u8, vec![0u8, 1, u8::MAX]);
1233        round_trip!(u16, vec![0u16, 1, u16::MAX]);
1234        round_trip!(u32, vec![0u32, 1, u32::MAX]);
1235        round_trip!(u64, vec![0u64, 1, u64::MAX]);
1236        round_trip!(f32, vec![f32::MIN, -0.5, 0.0, 0.5, f32::MAX]);
1237        round_trip!(f64, vec![f64::MIN, -0.5, 0.0, 0.5, f64::MAX]);
1238    }
1239
1240    /// A value that will not fit the requested type is an error, not a
1241    /// truncation.
1242    #[test]
1243    fn reading_an_array_as_too_narrow_a_type_is_refused() {
1244        let mut builder = AsdfBuilder::new();
1245        builder.set_array("data", &[1i64, 70_000, 3]).unwrap();
1246        let file = round_trip(&builder);
1247
1248        assert_eq!(file.read_array_of::<i64>("data").unwrap(), [1, 70_000, 3]);
1249        assert!(file.read_array_of::<i16>("data").is_err(), "70000 has no i16");
1250        // The ones that do fit are not affected.
1251        assert_eq!(file.read_array_of::<i32>("data").unwrap(), [1, 70_000, 3]);
1252    }
1253
1254    #[test]
1255    fn a_shaped_array_keeps_its_shape() {
1256        let mut builder = AsdfBuilder::new();
1257        let values: Vec<u8> = (0..6).collect();
1258        builder.set_array_shaped("grid", &values, &[2, 3]).unwrap();
1259
1260        // The length has to match the shape.
1261        assert!(builder.set_array_shaped("bad", &values, &[2, 4]).is_err());
1262
1263        let file = round_trip(&builder);
1264        let tree = file.tree().unwrap().unwrap();
1265        let array = tree.get("grid").unwrap().as_ndarray().unwrap();
1266        assert_eq!(array.resolved_shape(None).unwrap(), vec![2, 3]);
1267        assert_eq!(file.read_array_of::<u8>("grid").unwrap(), values);
1268    }
1269
1270    /// Open, change something, write it back: the workflow the API could not
1271    /// do at all before `edit`.
1272    #[test]
1273    fn a_file_can_be_opened_edited_and_written_back() {
1274        let mut original = AsdfBuilder::new();
1275        original.set_str("meta/observer", "A. Eddington").unwrap();
1276        original.set_array("data", &[1u16, 2, 3]).unwrap();
1277        let file = round_trip(&original);
1278
1279        let mut edited = file.edit().unwrap();
1280        edited.set_str("meta/observer", "M. Curie").unwrap();
1281        edited.set_i64("meta/exposure", 300).unwrap();
1282        let rewritten = round_trip(&edited);
1283
1284        let tree = rewritten.tree().unwrap().unwrap();
1285        assert_eq!(tree.get("meta/observer").and_then(|v| v.as_str()), Some("M. Curie"));
1286        assert_eq!(tree.get("meta/exposure").and_then(|v| v.as_i64()), Some(300));
1287
1288        // The block came across, and the `source: 0` in the tree still
1289        // points at it.
1290        assert_eq!(rewritten.block_count(), 1);
1291        assert_eq!(rewritten.read_array_of::<u16>("data").unwrap(), [1, 2, 3]);
1292    }
1293
1294    /// Editing a file with several blocks keeps their indices aligned.
1295    #[test]
1296    fn editing_preserves_block_indices() {
1297        let mut original = AsdfBuilder::new();
1298        original.set_array("first", &[1u8, 2]).unwrap();
1299        original.set_array("second", &[10u8, 20, 30]).unwrap();
1300        let file = round_trip(&original);
1301
1302        let mut edited = file.edit().unwrap();
1303        // A block added while editing goes after the ones already there.
1304        edited.set_array("third", &[7u8]).unwrap();
1305        let rewritten = round_trip(&edited);
1306
1307        assert_eq!(rewritten.block_count(), 3);
1308        assert_eq!(rewritten.read_array_of::<u8>("first").unwrap(), [1, 2]);
1309        assert_eq!(rewritten.read_array_of::<u8>("second").unwrap(), [10, 20, 30]);
1310        assert_eq!(rewritten.read_array_of::<u8>("third").unwrap(), [7]);
1311    }
1312
1313    /// Editing recompresses, so a builder's compression setting applies to
1314    /// blocks that were already there.
1315    #[test]
1316    fn editing_can_change_a_files_compression() {
1317        let mut original = AsdfBuilder::new();
1318        original.set_array("data", &vec![0u8; 4096]).unwrap();
1319        let file = round_trip(&original);
1320        assert_eq!(file.block_compression(0).unwrap(), Compression::None);
1321
1322        // `with_compression` is for new arrays; `recompress` is what
1323        // changes the blocks that were already there.
1324        assert_eq!(
1325            round_trip(&file.edit().unwrap().with_compression(Compression::Zlib))
1326                .block_compression(0)
1327                .unwrap(),
1328            Compression::None,
1329            "an existing block keeps its own compression"
1330        );
1331
1332        let edited = file.edit().unwrap().recompress(Compression::Zlib);
1333        let rewritten = round_trip(&edited);
1334
1335        assert_eq!(rewritten.block_compression(0).unwrap(), Compression::Zlib);
1336        assert!(rewritten.block_raw(0).unwrap().len() < 4096, "it should have shrunk");
1337        assert_eq!(rewritten.read_array_of::<u8>("data").unwrap(), vec![0u8; 4096]);
1338    }
1339
1340    #[test]
1341    fn info_and_events_are_reachable_from_rust() {
1342        let mut builder = AsdfBuilder::new();
1343        builder.set_array("data", &[1u8, 2, 3]).unwrap();
1344        let file = round_trip(&builder);
1345
1346        let rendered = file
1347            .info(InfoOptions { print_tree: true, print_blocks: true, verify_checksums: false })
1348            .unwrap();
1349        assert!(rendered.contains("data"), "{rendered}");
1350
1351        let stream = file.events(EventOptions::default());
1352        let names: Vec<&str> = stream.iter().map(Event::type_name).collect();
1353        assert_eq!(names.first(), Some(&"ASDF_ASDF_VERSION_EVENT"));
1354        assert_eq!(names.last(), Some(&"ASDF_END_EVENT"));
1355        assert!(names.contains(&"ASDF_BLOCK_EVENT"));
1356    }
1357
1358    /// The core schemas are reachable from Rust, not only from C.
1359    #[test]
1360    fn the_provenance_schemas_read_from_rust() {
1361        let source = "#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n\
1362%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n\
1363asdf_library: !core/software-1.0.0 {name: asdf, version: 4.1.0}\n\
1364history:\n  \
1365extensions:\n  \
1366- !core/extension_metadata-1.0.0\n    \
1367extension_class: asdf.extension._manifest.ManifestExtension\n    \
1368software: !core/software-1.0.0 {name: asdf, version: 4.1.0}\n  \
1369entries:\n  \
1370- !core/history_entry-1.0.0\n    \
1371description: made this file\n    \
1372time: !<tag:stsci.edu:asdf/time/time-1.4.0> '2025-07-23 11:56:15+00:00'\n\
1373...\n";
1374        let file = AsdfFile::from_bytes(source.as_bytes().to_vec()).unwrap();
1375        let tree = file.tree().unwrap().unwrap();
1376
1377        // Whole-file provenance in one call.
1378        let meta = tree.meta().unwrap();
1379        assert_eq!(meta.asdf_library.as_ref().unwrap().name, "asdf");
1380        assert_eq!(meta.history.extensions.len(), 1);
1381        assert_eq!(meta.history.entries.len(), 1);
1382
1383        let entry = &meta.history.entries[0];
1384        assert_eq!(entry.description.as_deref(), Some("made this file"));
1385        assert_eq!(entry.time.as_ref().unwrap().civil.unwrap().unix_seconds, 1_753_271_775);
1386
1387        // Or one value at a time, by path.
1388        let library = tree.get("asdf_library").unwrap().as_software().unwrap();
1389        assert_eq!(library.version, "4.1.0");
1390
1391        let ext = tree.get("history/extensions/0").unwrap().as_extension_metadata().unwrap();
1392        assert_eq!(ext.extension_class, "asdf.extension._manifest.ManifestExtension");
1393        assert!(ext.package.is_none(), "this record names no package");
1394
1395        let time = tree.get("history/entries/0/time").unwrap().as_time().unwrap();
1396        assert_eq!(time.format, TimeFormat::Iso);
1397        assert_eq!(time.scale, TimeScale::Utc);
1398
1399        // A value that is not one of these says so rather than guessing.
1400        assert!(tree.get("asdf_library").unwrap().as_time().is_none());
1401        assert!(tree.get("history").unwrap().as_software().is_none());
1402    }
1403
1404    /// The stamp a written file carries reads back as a `Software`.
1405    #[test]
1406    fn a_written_files_stamp_reads_back_as_software() {
1407        let file = round_trip(&AsdfBuilder::new());
1408        let tree = file.tree().unwrap().unwrap();
1409
1410        let library = tree.meta().unwrap().asdf_library.expect("asdf_library");
1411        assert_eq!(library, Software::this_library());
1412    }
1413
1414    #[test]
1415    fn writes_and_reads_scalars() {
1416        let mut builder = AsdfBuilder::new();
1417        builder.set_str("name", "Dennis Richie").unwrap();
1418        builder.set_i64("foo", 42).unwrap();
1419        builder.set_u64("big", 5_000_000_000).unwrap();
1420        builder.set_f64("ratio", 1.5).unwrap();
1421        builder.set_bool("flag", true).unwrap();
1422        builder.set_null("nothing").unwrap();
1423
1424        let file = round_trip(&builder);
1425        let tree = file.tree().unwrap().unwrap();
1426
1427        assert_eq!(tree.get("name").unwrap().as_str(), Some("Dennis Richie"));
1428        assert_eq!(tree.get("foo").unwrap().as_i64(), Some(42));
1429        assert_eq!(tree.get("big").unwrap().as_u64(), Some(5_000_000_000));
1430        assert_eq!(tree.get("ratio").unwrap().as_f64(), Some(1.5));
1431        assert_eq!(tree.get("flag").unwrap().as_bool(), Some(true));
1432        assert!(tree.get("nothing").unwrap().is_null());
1433        assert!(tree.get("missing").is_none());
1434    }
1435
1436    #[test]
1437    fn a_numeric_string_stays_a_string() {
1438        let mut builder = AsdfBuilder::new();
1439        builder.set_str("version", "42").unwrap();
1440
1441        let file = round_trip(&builder);
1442        let tree = file.tree().unwrap().unwrap();
1443        let value = tree.get("version").unwrap();
1444        assert_eq!(value.as_str(), Some("42"), "quoting was lost");
1445        assert_eq!(value.as_i64(), None, "a string must not read as an integer");
1446    }
1447
1448    #[test]
1449    fn nested_paths_are_materialised() {
1450        let mut builder = AsdfBuilder::new();
1451        builder.set_i64("meta/observation/exposure", 300).unwrap();
1452
1453        let file = round_trip(&builder);
1454        let tree = file.tree().unwrap().unwrap();
1455        assert_eq!(tree.get("meta/observation/exposure").unwrap().as_i64(), Some(300));
1456        assert!(tree.get("meta").unwrap().is_mapping());
1457    }
1458
1459    #[test]
1460    fn writes_and_reads_arrays() {
1461        let squares: Vec<u64> = (0..100u64).map(|i| i * i).collect();
1462        let mut builder = AsdfBuilder::new();
1463        builder.set_array_u64("powers/squares", &squares).unwrap();
1464
1465        let file = round_trip(&builder);
1466        let tree = file.tree().unwrap().unwrap();
1467
1468        let value = tree.get("powers/squares").unwrap();
1469        assert!(value.has_tag("core/ndarray"));
1470
1471        let array = value.as_ndarray().unwrap();
1472        let read_back = file.read_array_i64(&array).unwrap();
1473        assert_eq!(read_back.len(), 100);
1474        assert_eq!(read_back[10], 100);
1475        assert_eq!(read_back.iter().sum::<i64>(), squares.iter().sum::<u64>() as i64);
1476    }
1477
1478    #[test]
1479    fn writes_and_reads_float_arrays() {
1480        let values: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.25).collect();
1481        let mut builder = AsdfBuilder::new();
1482        builder.set_array_f64("data", &values).unwrap();
1483
1484        let file = round_trip(&builder);
1485        let tree = file.tree().unwrap().unwrap();
1486        let array = tree.get("data").unwrap().as_ndarray().unwrap();
1487        assert_eq!(file.read_array_f64(&array).unwrap(), values);
1488    }
1489
1490    #[test]
1491    fn multi_dimensional_arrays_keep_their_shape() {
1492        let values: Vec<f64> = (0..12).map(f64::from).collect();
1493        let mut builder = AsdfBuilder::new();
1494        builder.set_array_f64_shaped("image", &values, &[3, 4]).unwrap();
1495
1496        let file = round_trip(&builder);
1497        let tree = file.tree().unwrap().unwrap();
1498        let array = tree.get("image").unwrap().as_ndarray().unwrap();
1499
1500        assert_eq!(array.resolved_shape(None).unwrap(), vec![3, 4]);
1501        assert_eq!(file.read_array_f64(&array).unwrap(), values);
1502    }
1503
1504    #[test]
1505    fn a_shape_that_does_not_match_the_data_is_refused() {
1506        let mut builder = AsdfBuilder::new();
1507        let err = builder.set_array_f64_shaped("image", &[1.0, 2.0], &[3, 4]).unwrap_err();
1508        assert_eq!(err.code(), ErrorCode::InvalidArgument);
1509    }
1510
1511    #[test]
1512    fn arrays_can_be_compressed() {
1513        for compression in asdf_core::compression::available() {
1514            let values: Vec<u64> = (0..1000u64).map(|i| i % 7).collect();
1515            let mut builder = AsdfBuilder::new().with_compression(compression);
1516            builder.set_array_u64("data", &values).unwrap();
1517
1518            let file = round_trip(&builder);
1519            assert_eq!(file.block_compression(0).unwrap(), compression);
1520            assert_eq!(file.verify_block(0).unwrap(), ChecksumStatus::Valid);
1521
1522            let tree = file.tree().unwrap().unwrap();
1523            let array = tree.get("data").unwrap().as_ndarray().unwrap();
1524            let read_back = file.read_array_i64(&array).unwrap();
1525            assert_eq!(read_back.len(), values.len(), "{compression:?}");
1526            assert_eq!(read_back[3], 3, "{compression:?}");
1527        }
1528    }
1529
1530    #[test]
1531    fn iterates_mappings_and_sequences() {
1532        let mut builder = AsdfBuilder::new();
1533        builder.set_i64("a", 1).unwrap();
1534        builder.set_i64("b", 2).unwrap();
1535        builder.set_i64("c", 3).unwrap();
1536
1537        let file = round_trip(&builder);
1538        let tree = file.tree().unwrap().unwrap();
1539        let root = tree.root().unwrap();
1540
1541        // `asdf_library` is appended by the writer, stamping the file with
1542        // what wrote it, so it comes last.
1543        let keys: Vec<&str> = root.entries().map(|(k, _)| k).collect();
1544        assert_eq!(keys, ["a", "b", "c", "asdf_library"], "insertion order must survive");
1545
1546        let values: Vec<i64> = root.entries().filter_map(|(_, v)| v.as_i64()).collect();
1547        assert_eq!(values, [1, 2, 3]);
1548    }
1549
1550    /// Every file we write says what wrote it. Readers act on that: the
1551    /// workaround for the Python checksum bug keys off exactly this field.
1552    #[test]
1553    fn written_files_record_what_wrote_them() {
1554        let builder = AsdfBuilder::new();
1555        let file = round_trip(&builder);
1556        let tree = file.tree().unwrap().unwrap();
1557
1558        let library = tree.get("asdf_library").expect("asdf_library");
1559        assert!(library.has_tag("core/software"));
1560        assert_eq!(library.get("name").and_then(|v| v.as_str()), Some("libasdf-rs"));
1561        assert!(library.get("version").and_then(|v| v.as_str()).is_some());
1562        assert!(library.get("homepage").and_then(|v| v.as_str()).is_some());
1563    }
1564
1565    /// A tree that already names its writer keeps it -- rewriting someone
1566    /// else's file must not claim authorship of it.
1567    #[test]
1568    fn an_existing_asdf_library_is_left_alone() {
1569        let source = "#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n\
1570%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n\
1571asdf_library: !core/software-1.0.0 {name: asdf, version: 4.1.0}\n\
1572x: 1\n...\n";
1573        let original = AsdfFile::from_bytes(source.as_bytes().to_vec()).unwrap();
1574        let tree = original.tree().unwrap().unwrap();
1575
1576        let mut builder = AsdfBuilder::new();
1577        *builder.document_mut() = tree.document().clone();
1578        let rewritten = round_trip(&builder);
1579
1580        let tree = rewritten.tree().unwrap().unwrap();
1581        let library = tree.get("asdf_library").unwrap();
1582        assert_eq!(library.get("name").and_then(|v| v.as_str()), Some("asdf"));
1583    }
1584
1585    #[test]
1586    fn sequences_index_forwards_and_backwards() {
1587        let doc = yaml::parse_document("s: [10, 20, 30]\n").unwrap();
1588        let tree = Tree { document: doc };
1589        let seq = tree.get("s").unwrap();
1590
1591        assert_eq!(seq.len(), Some(3));
1592        assert_eq!(seq.at(0).unwrap().as_i64(), Some(10));
1593        assert_eq!(seq.at(-1).unwrap().as_i64(), Some(30));
1594        assert!(seq.at(3).is_none());
1595
1596        let all: Vec<i64> = seq.items().filter_map(|v| v.as_i64()).collect();
1597        assert_eq!(all, [10, 20, 30]);
1598    }
1599
1600    #[test]
1601    fn aliases_are_visible_and_resolve() {
1602        let doc = yaml::parse_document("shared: &a {x: 1}\nother: *a\n").unwrap();
1603        let tree = Tree { document: doc };
1604
1605        let other = tree.get("other").unwrap();
1606        assert!(other.is_alias());
1607        // Reading through the alias sees the shared value.
1608        assert_eq!(other.get("x").unwrap().as_i64(), Some(1));
1609        assert_eq!(tree.get("other/x").unwrap().as_i64(), Some(1));
1610    }
1611
1612    #[test]
1613    fn tags_are_matched_without_their_version() {
1614        let doc = yaml::parse_document(
1615            "%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n\
1616             d: !core/ndarray-1.0.0\n  source: 0\n...\n",
1617        )
1618        .unwrap();
1619        let tree = Tree { document: doc };
1620        let value = tree.get("d").unwrap();
1621        assert!(value.has_tag("core/ndarray"));
1622        assert!(!value.has_tag("core/software"));
1623        assert_eq!(value.tag().unwrap().full(), "tag:stsci.edu:asdf/core/ndarray-1.0.0");
1624    }
1625
1626    #[test]
1627    fn trees_render_back_to_yaml() {
1628        let mut builder = AsdfBuilder::new();
1629        builder.set_i64("foo", 42).unwrap();
1630
1631        let file = round_trip(&builder);
1632        let tree = file.tree().unwrap().unwrap();
1633        let text = tree.to_yaml().unwrap();
1634        assert!(text.contains("foo: 42"), "{text}");
1635        assert!(text.starts_with("%YAML 1.1"), "{text}");
1636    }
1637
1638    #[test]
1639    fn value_equality_ignores_presentation() {
1640        let a = Tree { document: yaml::parse_document("a: {x: 1, y: 2}\n").unwrap() };
1641        let b = Tree { document: yaml::parse_document("a:\n  x: 1\n  y: 2\n").unwrap() };
1642        assert!(a.value_eq(&b));
1643
1644        let c = Tree { document: yaml::parse_document("a: {x: 1, y: 3}\n").unwrap() };
1645        assert!(!a.value_eq(&c));
1646    }
1647
1648    #[test]
1649    fn versions_are_reported() {
1650        let builder = AsdfBuilder::new();
1651        let file = round_trip(&builder);
1652        assert_eq!(file.format_version().triple(), (1, 0, 0));
1653        assert_eq!(file.standard_version().unwrap().triple(), (1, 6, 0));
1654    }
1655
1656    #[test]
1657    fn raw_blocks_round_trip() {
1658        let mut builder = AsdfBuilder::new();
1659        let index = builder.add_block(b"arbitrary bytes".to_vec());
1660        assert_eq!(index, 0);
1661
1662        let file = round_trip(&builder);
1663        assert_eq!(file.block_count(), 1);
1664        assert_eq!(&*file.block_data(0).unwrap(), b"arbitrary bytes");
1665    }
1666}