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