1#![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
93pub type Result<T> = std::result::Result<T, Error>;
95
96pub trait ArrayElement: sealed::Sealed + Copy {
104 const SCALAR: ScalarType;
106
107 fn to_bytes(self) -> Vec<u8>;
109
110 fn from_element(element: &Element) -> Option<Self>;
116}
117
118mod sealed {
119 pub trait Sealed {}
120}
121
122macro_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
154macro_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 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#[derive(Debug)]
190pub struct AsdfFile {
191 reader: Reader,
192}
193
194impl AsdfFile {
195 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
200 Ok(Self { reader: Reader::open(path)? })
201 }
202
203 pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
205 Ok(Self { reader: Reader::from_bytes(bytes)? })
206 }
207
208 pub fn format_version(&self) -> &Version {
210 &self.reader.layout().format_version
211 }
212
213 pub fn standard_version(&self) -> Option<&Version> {
215 self.reader.layout().standard_version.as_ref()
216 }
217
218 pub fn tree(&self) -> Result<Option<Tree>> {
223 Ok(self.reader.tree()?.map(|document| Tree { document }))
224 }
225
226 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 pub fn block_count(&self) -> usize {
237 self.reader.block_count()
238 }
239
240 pub fn block_data(&self, index: usize) -> Result<Cow<'_, [u8]>> {
244 self.reader.block_data(index)
245 }
246
247 pub fn block_raw(&self, index: usize) -> Result<&[u8]> {
249 self.reader.block_raw(index)
250 }
251
252 pub fn block_compression(&self, index: usize) -> Result<Compression> {
254 self.reader.block_compression(index)
255 }
256
257 pub fn verify_block(&self, index: usize) -> Result<ChecksumStatus> {
262 Ok(self.reader.verify_block_checksum(index)?.0)
263 }
264
265 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 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 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 pub fn read_array_f64(&self, array: &Ndarray) -> Result<Vec<f64>> {
331 as_f64(self.read_array(array)?)
332 }
333
334 pub fn read_array_i64(&self, array: &Ndarray) -> Result<Vec<i64>> {
339 as_i64(self.read_array(array)?)
340 }
341
342 pub fn read_array_f64_at(&self, path: &str) -> Result<Vec<f64>> {
344 as_f64(self.read_array_at(path)?)
345 }
346
347 pub fn read_array_i64_at(&self, path: &str) -> Result<Vec<i64>> {
349 as_i64(self.read_array_at(path)?)
350 }
351
352 pub fn read_array_of<T: ArrayElement>(&self, path: &str) -> Result<Vec<T>> {
366 as_type(self.read_array_at(path)?)
367 }
368
369 pub fn edit(&self) -> Result<AsdfBuilder> {
388 let document = self.reader.tree()?.unwrap_or_else(Document::new_asdf);
389
390 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 pub fn info(&self, options: InfoOptions) -> Result<String> {
408 asdf_core::info::render(&self.reader, options)
409 }
410
411 pub fn events(&self, options: EventOptions) -> Vec<Event> {
419 asdf_core::events::events_from(self.reader.bytes(), self.reader.layout(), options)
420 }
421}
422
423fn 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
438fn 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
455fn 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#[derive(Clone, Debug)]
476pub struct Tree {
477 document: Document,
478}
479
480impl Tree {
481 pub fn root(&self) -> Option<Value<'_>> {
483 self.document.root().map(|node| Value { document: &self.document, node })
484 }
485
486 pub fn get(&self, path: &str) -> Option<Value<'_>> {
491 self.document.lookup_str(path).map(|node| Value { document: &self.document, node })
492 }
493
494 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 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 pub fn document(&self) -> &Document {
518 &self.document
519 }
520
521 pub fn value_eq(&self, other: &Tree) -> bool {
526 yaml::compare(&self.document, &other.document, CompareOptions::default()).is_equal()
527 }
528
529 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#[derive(Clone, Copy, Debug)]
538pub struct Value<'a> {
539 document: &'a Document,
540 node: NodeId,
541}
542
543impl<'a> Value<'a> {
544 pub fn tag(&self) -> Option<&'a Tag> {
546 self.document.tag_of(self.node)
547 }
548
549 pub fn has_tag(&self, name: &str) -> bool {
554 self.tag().is_some_and(|t| t.split_version().0 == name)
555 }
556
557 pub fn as_raw_str(&self) -> Option<&'a str> {
559 self.document.resolved(self.node).as_str()
560 }
561
562 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 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 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 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 pub fn as_bool(&self) -> Option<bool> {
604 match self.resolved()? {
605 Resolved::Bool(v) => Some(v),
606 _ => None,
607 }
608 }
609
610 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 pub fn is_mapping(&self) -> bool {
625 self.document.resolved(self.node).is_mapping()
626 }
627
628 pub fn is_sequence(&self) -> bool {
630 self.document.resolved(self.node).is_sequence()
631 }
632
633 pub fn len(&self) -> Option<usize> {
635 self.document.container_len(self.node)
636 }
637
638 pub fn is_empty(&self) -> Option<bool> {
640 self.len().map(|n| n == 0)
641 }
642
643 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 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 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 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 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 pub fn as_ndarray(&self) -> Option<Ndarray> {
687 Ndarray::parse(self.document, self.node).ok()
688 }
689
690 pub fn as_software(&self) -> Option<Software> {
692 Software::parse(self.document, self.node).ok()
693 }
694
695 pub fn as_history_entry(&self) -> Option<HistoryEntry> {
697 HistoryEntry::parse(self.document, self.node).ok()
698 }
699
700 pub fn as_extension_metadata(&self) -> Option<ExtensionMetadata> {
702 ExtensionMetadata::parse(self.document, self.node).ok()
703 }
704
705 pub fn as_time(&self) -> Option<Time> {
711 Time::parse(self.document, self.node).ok()
712 }
713
714 pub fn is_alias(&self) -> bool {
716 self.document.node(self.node).is_alias()
717 }
718}
719
720#[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 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 pub fn with_compression(mut self, compression: Compression) -> Self {
750 self.compression = compression;
751 self
752 }
753
754 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 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 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 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 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 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 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 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 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 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 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 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 pub fn set_array_u64(&mut self, path: &str, values: &[u64]) -> Result<()> {
894 self.set_array(path, values)
895 }
896
897 pub fn set_array_i64(&mut self, path: &str, values: &[i64]) -> Result<()> {
899 self.set_array(path, values)
900 }
901
902 pub fn set_array_f64(&mut self, path: &str, values: &[f64]) -> Result<()> {
904 self.set_array(path, values)
905 }
906
907 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 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 pub fn to_bytes(&self) -> Result<Vec<u8>> {
935 self.writer().to_bytes()
936 }
937
938 pub fn write_to_path(&self, path: impl AsRef<Path>) -> Result<()> {
940 self.writer().write_to_path(path)
941 }
942
943 pub fn write_to(&self, sink: &mut impl std::io::Write) -> Result<()> {
945 self.writer().write_to(sink)
946 }
947}
948
949pub fn native_byte_order() -> ByteOrder {
951 ByteOrder::native()
952}
953
954pub 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 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 assert!(file.read_array(&array).is_err());
984
985 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 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 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 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 #[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 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 #[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 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 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 #[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 assert_eq!(rewritten.block_count(), 1);
1154 assert_eq!(rewritten.read_array_of::<u16>("data").unwrap(), [1, 2, 3]);
1155 }
1156
1157 #[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 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 #[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 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 #[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 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 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 assert!(tree.get("asdf_library").unwrap().as_time().is_none());
1264 assert!(tree.get("history").unwrap().as_software().is_none());
1265 }
1266
1267 #[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 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 #[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 #[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 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}