Skip to main content

asdf_core/
writer.rs

1//! Writing ASDF files.
2//!
3//! The layout is assembled in the order the standard prescribes: the `#ASDF`
4//! header line, the `#ASDF_STANDARD` comment, the YAML tree, the binary
5//! blocks, and finally the block index.
6
7use std::io::Write;
8use std::path::Path;
9
10use asdf_yaml::{Document, EmitOptions, TagHandle, emit_with};
11
12use crate::block::header::{BlockHeader, CHECKSUM_SIZE};
13use crate::compression::Compression;
14use crate::core::provenance::Software;
15use crate::error::{Result, err};
16use crate::layout::write_block_index;
17use crate::version::{ASDF_FORMAT_VERSION, ASDF_STANDARD_VERSION};
18
19/// A block queued for writing.
20#[derive(Clone, Debug)]
21pub struct PendingBlock {
22    /// The data, uncompressed unless [`PendingBlock::already_compressed`] is
23    /// set.
24    pub data: Vec<u8>,
25    /// How to compress it on the way out, or how it is already compressed.
26    pub compression: Compression,
27    /// Space to reserve, which may exceed the used size so the block can grow
28    /// later without moving everything after it. Zero means "use the used
29    /// size".
30    pub allocated_size: u64,
31    /// `data` already holds compressed bytes, to be written verbatim.
32    ///
33    /// This is how a block is copied from one file to another without a
34    /// decompress/recompress round trip, which would be wasteful and could
35    /// not reproduce the original bytes.
36    pub already_compressed: bool,
37    /// The uncompressed size, needed only when [`Self::already_compressed`]
38    /// is set: it is what the header must record and cannot be measured from
39    /// the bytes at hand.
40    pub uncompressed_size: u64,
41}
42
43impl PendingBlock {
44    /// A block of uncompressed data.
45    pub fn new(data: Vec<u8>) -> Self {
46        Self {
47            data,
48            compression: Compression::None,
49            allocated_size: 0,
50            already_compressed: false,
51            uncompressed_size: 0,
52        }
53    }
54
55    /// A block compressed with the given method.
56    pub fn compressed(data: Vec<u8>, compression: Compression) -> Self {
57        Self { compression, ..Self::new(data) }
58    }
59
60    /// A block whose bytes are already compressed, written verbatim.
61    ///
62    /// `uncompressed_size` is what the data decompresses to; the header
63    /// records it and a reader needs it to size its output buffer.
64    pub fn precompressed(data: Vec<u8>, compression: Compression, uncompressed_size: u64) -> Self {
65        Self { compression, already_compressed: true, uncompressed_size, ..Self::new(data) }
66    }
67}
68
69/// How to write a file.
70#[derive(Clone, Debug)]
71pub struct WriteOptions {
72    /// The version written on the `#ASDF` header line.
73    pub format_version: String,
74    /// The version written on the `#ASDF_STANDARD` comment line.
75    pub standard_version: String,
76    /// Write a block index after the last block.
77    pub write_block_index: bool,
78    /// Compute and store each block's MD5 checksum.
79    pub write_checksums: bool,
80    /// How the YAML tree is laid out.
81    pub emit: EmitOptions,
82    /// Bytes of padding between the tree and the first block, so the tree can
83    /// grow later without rewriting the whole file.
84    pub tree_padding: usize,
85    /// The `asdf_library` provenance to record, unless the tree already has
86    /// one.
87    ///
88    /// Every ASDF writer stamps the file with what wrote it, and readers act
89    /// on it: the workaround for the Python checksum bug keys off exactly
90    /// this field. `None` writes nothing, which is what a caller reproducing
91    /// another writer's output wants.
92    pub asdf_library: Option<Software>,
93}
94
95impl Default for WriteOptions {
96    fn default() -> Self {
97        Self {
98            format_version: ASDF_FORMAT_VERSION.to_string(),
99            standard_version: ASDF_STANDARD_VERSION.to_string(),
100            write_block_index: true,
101            write_checksums: true,
102            emit: EmitOptions::default(),
103            tree_padding: 0,
104            asdf_library: Some(Software::this_library()),
105        }
106    }
107}
108
109/// Assembles an ASDF file.
110#[derive(Debug)]
111pub struct Writer {
112    document: Option<Document>,
113    blocks: Vec<PendingBlock>,
114    options: WriteOptions,
115}
116
117impl Default for Writer {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123impl Writer {
124    /// A writer with no tree and no blocks.
125    pub fn new() -> Self {
126        Self { document: None, blocks: Vec::new(), options: WriteOptions::default() }
127    }
128
129    /// A writer for an existing tree.
130    pub fn from_document(document: Document) -> Self {
131        Self { document: Some(document), blocks: Vec::new(), options: WriteOptions::default() }
132    }
133
134    /// Replace the write options.
135    pub fn with_options(mut self, options: WriteOptions) -> Self {
136        self.options = options;
137        self
138    }
139
140    /// Set the tree to write.
141    pub fn set_document(&mut self, document: Document) {
142        self.document = Some(document);
143    }
144
145    /// The tree being written, if any.
146    pub fn document(&self) -> Option<&Document> {
147        self.document.as_ref()
148    }
149
150    /// The tree being written, for mutation.
151    pub fn document_mut(&mut self) -> Option<&mut Document> {
152        self.document.as_mut()
153    }
154
155    /// Queue a block, returning the index it will have in the file.
156    ///
157    /// That index is what an ndarray's `source` refers to.
158    pub fn add_block(&mut self, block: PendingBlock) -> usize {
159        self.blocks.push(block);
160        self.blocks.len() - 1
161    }
162
163    /// The number of blocks queued.
164    pub fn block_count(&self) -> usize {
165        self.blocks.len()
166    }
167
168    /// Assemble the whole file in memory.
169    pub fn to_bytes(&self) -> Result<Vec<u8>> {
170        let mut out = Vec::new();
171
172        // Header line, then the standard version as a comment.
173        out.extend_from_slice(b"#ASDF ");
174        out.extend_from_slice(self.options.format_version.as_bytes());
175        out.push(b'\n');
176        out.extend_from_slice(b"#ASDF_STANDARD ");
177        out.extend_from_slice(self.options.standard_version.as_bytes());
178        out.push(b'\n');
179
180        if let Some(doc) = &self.document {
181            // A tree needs the directives and the `...` terminator: a reader
182            // finds where the tree ends by searching for the latter.
183            let mut options = self.options.emit.clone();
184            options.directives = true;
185            options.explicit_start = true;
186            options.explicit_end = true;
187
188            let mut doc = doc.clone();
189            if doc.version.is_none() {
190                doc.version = Some(asdf_yaml::YamlVersion::V1_1);
191            }
192            if doc.tag_handles.is_empty() {
193                doc.tag_handles.push(TagHandle::asdf_default());
194            }
195            // Stamp the file with what wrote it, unless the tree already
196            // says -- a caller rewriting someone else's file keeps theirs.
197            if let Some(software) = &self.options.asdf_library
198                && let Some(root) = doc.root()
199                && doc.node(root).is_mapping()
200                && doc.mapping_get(root, "asdf_library").is_none()
201            {
202                let node = software.to_node(&mut doc);
203                doc.mapping_set(root, "asdf_library", node);
204            }
205
206            let text = emit_with(&doc, &options)
207                .map_err(|e| err!(YamlParseFailed, "could not emit the tree: {e}"))?;
208            out.extend_from_slice(text.as_bytes());
209        }
210
211        // Optional padding, so the tree can grow without moving the blocks.
212        // The standard recommends spaces, which read as empty space.
213        if !self.blocks.is_empty() && self.options.tree_padding > 0 {
214            out.resize(out.len() + self.options.tree_padding, b' ');
215            out.push(b'\n');
216        }
217
218        let mut offsets = Vec::with_capacity(self.blocks.len());
219        for block in &self.blocks {
220            offsets.push(out.len() as u64);
221            self.write_block(&mut out, block)?;
222        }
223
224        // The index is forbidden when there are no blocks, and pointless
225        // when it was turned off.
226        if self.options.write_block_index && !offsets.is_empty() {
227            out.extend_from_slice(&write_block_index(&offsets));
228        }
229        Ok(out)
230    }
231
232    /// Append one block's header and data.
233    fn write_block(&self, out: &mut Vec<u8>, block: &PendingBlock) -> Result<()> {
234        // Bytes that are already compressed go out as they came in: a
235        // decompress/recompress round trip would waste the work and could
236        // not be relied on to reproduce them.
237        let stored = if block.already_compressed {
238            alloc::borrow::Cow::Borrowed(block.data.as_slice())
239        } else {
240            alloc::borrow::Cow::Owned(block.compression.compress(&block.data)?)
241        };
242
243        let used_size = stored.len() as u64;
244        let allocated_size = block.allocated_size.max(used_size);
245        let data_size = if block.already_compressed {
246            block.uncompressed_size
247        } else {
248            block.data.len() as u64
249        };
250
251        let mut header = BlockHeader { allocated_size, used_size, data_size, ..Default::default() };
252        header.set_compression(block.compression.name())?;
253
254        if self.options.write_checksums {
255            // The specification means the digest to cover the used data as
256            // stored, which for a compressed block is the compressed bytes.
257            header.checksum = md5_of(&stored);
258        }
259
260        header.write(out);
261        out.extend_from_slice(&stored);
262
263        // Reserved-but-unused space is left as zeros; the standard does not
264        // constrain its contents.
265        let padding = allocated_size - used_size;
266        out.resize(out.len() + padding as usize, 0);
267        Ok(())
268    }
269
270    /// Write the file to a stream.
271    pub fn write_to(&self, sink: &mut impl Write) -> Result<()> {
272        let bytes = self.to_bytes()?;
273        sink.write_all(&bytes)?;
274        Ok(())
275    }
276
277    /// Write the file to a path, replacing anything already there.
278    pub fn write_to_path(&self, path: impl AsRef<Path>) -> Result<()> {
279        let bytes = self.to_bytes()?;
280        std::fs::write(path, bytes)?;
281        Ok(())
282    }
283}
284
285fn md5_of(data: &[u8]) -> [u8; CHECKSUM_SIZE] {
286    use md5::{Digest, Md5};
287    let mut hasher = Md5::new();
288    hasher.update(data);
289    hasher.finalize().into()
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use crate::reader::{ChecksumStatus, Reader};
296    use asdf_yaml::{CompareOptions, Tag, compare, parse_document};
297
298    fn tree(yaml: &str) -> Document {
299        parse_document(yaml).unwrap()
300    }
301
302    #[test]
303    fn writes_a_readable_file() {
304        let doc =
305            tree("%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\nfoo: 42\n...\n");
306        let writer = Writer::from_document(doc);
307        let bytes = writer.to_bytes().unwrap();
308
309        let reader = Reader::from_bytes(bytes).unwrap();
310        assert_eq!(reader.layout().format_version.triple(), (1, 0, 0));
311        assert_eq!(reader.layout().standard_version.as_ref().unwrap().triple(), (1, 6, 0));
312
313        let read_back = reader.tree().unwrap().unwrap();
314        let root = read_back.root().unwrap();
315        assert_eq!(read_back.tag_of(root).unwrap().full(), "tag:stsci.edu:asdf/core/asdf-1.1.0");
316        let foo = read_back.mapping_get(root, "foo").unwrap();
317        assert_eq!(read_back.node(foo).as_str(), Some("42"));
318    }
319
320    #[test]
321    fn a_written_tree_reads_back_equal() {
322        let source = "%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n\
323                      name: Dennis Richie\nfoo: 42\nnested:\n  a: [1, 2, 3]\n\
324                      shared: &x {p: 1}\nalias: *x\n...\n";
325        let original = tree(source);
326        // No provenance stamp, so the tree that comes back is the tree that
327        // went in. `written_files_record_what_wrote_them` covers the stamp.
328        let options = WriteOptions { asdf_library: None, ..Default::default() };
329        let bytes =
330            Writer::from_document(original.clone()).with_options(options).to_bytes().unwrap();
331
332        let reader = Reader::from_bytes(bytes).unwrap();
333        let read_back = reader.tree().unwrap().unwrap();
334        let result = compare(&original, &read_back, CompareOptions::default());
335        assert!(result.is_equal(), "{result}");
336    }
337
338    /// Every file says what wrote it, unless the tree already does.
339    #[test]
340    fn a_written_file_records_what_wrote_it() {
341        let bytes = Writer::from_document(tree("x: 1\n")).to_bytes().unwrap();
342        let reader = Reader::from_bytes(bytes).unwrap();
343        let doc = reader.tree().unwrap().unwrap();
344        let root = doc.root().unwrap();
345
346        let library = doc.mapping_get(root, "asdf_library").expect("asdf_library");
347        assert_eq!(
348            doc.tag_of(library).map(asdf_yaml::Tag::full).as_deref(),
349            Some("tag:stsci.edu:asdf/core/software-1.0.0")
350        );
351        let name = doc.mapping_get(library, "name").unwrap();
352        assert_eq!(doc.resolved(name).as_str(), Some("libasdf-rs"));
353    }
354
355    #[test]
356    fn an_existing_provenance_stamp_is_kept() {
357        let source = "%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n\
358                      asdf_library: !core/software-1.0.0 {name: asdf, version: 4.1.0}\n\
359                      x: 1\n...\n";
360        let bytes = Writer::from_document(tree(source)).to_bytes().unwrap();
361        let reader = Reader::from_bytes(bytes).unwrap();
362        let doc = reader.tree().unwrap().unwrap();
363        let root = doc.root().unwrap();
364
365        let library = doc.mapping_get(root, "asdf_library").unwrap();
366        let name = doc.mapping_get(library, "name").unwrap();
367        assert_eq!(doc.resolved(name).as_str(), Some("asdf"), "the original writer must survive");
368    }
369
370    /// Bytes that are already compressed go out verbatim, with the
371    /// uncompressed size the caller declared recorded in the header.
372    ///
373    /// This is how a block is copied between files without a
374    /// decompress/recompress round trip -- which would waste the work and
375    /// could not be relied on to reproduce the original bytes.
376    #[test]
377    fn precompressed_blocks_are_written_as_they_came() {
378        let payload: Vec<u8> = (0..4096u32).map(|i| (i * 7) as u8).collect();
379        let compressed = Compression::Zlib.compress(&payload).unwrap();
380        assert!(compressed.len() < payload.len(), "the fixture must actually compress");
381
382        let mut writer = Writer::from_document(tree("a: 1\n"));
383        writer.add_block(PendingBlock::precompressed(
384            compressed.clone(),
385            Compression::Zlib,
386            payload.len() as u64,
387        ));
388
389        let reader = Reader::from_bytes(writer.to_bytes().unwrap()).unwrap();
390        // Stored exactly as handed over.
391        assert_eq!(reader.block_raw(0).unwrap(), &compressed[..]);
392        // And the header knows what it decompresses to, which a reader needs
393        // to size its buffer.
394        assert_eq!(reader.block(0).unwrap().header.data_size, payload.len() as u64);
395        assert_eq!(reader.block(0).unwrap().header.used_size, compressed.len() as u64);
396        assert_eq!(&*reader.block_data(0).unwrap(), &payload[..]);
397        assert_eq!(reader.block_compression(0).unwrap(), Compression::Zlib);
398    }
399
400    #[test]
401    fn writes_blocks_that_read_back_byte_for_byte() {
402        let mut writer = Writer::from_document(tree("a: 1\n"));
403        let first: Vec<u8> = (0..=255u8).collect();
404        let second = b"second block".to_vec();
405
406        assert_eq!(writer.add_block(PendingBlock::new(first.clone())), 0);
407        assert_eq!(writer.add_block(PendingBlock::new(second.clone())), 1);
408
409        let reader = Reader::from_bytes(writer.to_bytes().unwrap()).unwrap();
410        assert_eq!(reader.block_count(), 2);
411        assert_eq!(&*reader.block_data(0).unwrap(), &first[..]);
412        assert_eq!(&*reader.block_data(1).unwrap(), &second[..]);
413    }
414
415    #[test]
416    fn written_checksums_verify() {
417        let mut writer = Writer::from_document(tree("a: 1\n"));
418        writer.add_block(PendingBlock::new(vec![7u8; 1024]));
419
420        let reader = Reader::from_bytes(writer.to_bytes().unwrap()).unwrap();
421        let (status, _) = reader.verify_block_checksum(0).unwrap();
422        assert_eq!(status, ChecksumStatus::Valid);
423    }
424
425    #[test]
426    fn checksums_can_be_turned_off() {
427        let options = WriteOptions { write_checksums: false, ..Default::default() };
428        let mut writer = Writer::from_document(tree("a: 1\n")).with_options(options);
429        writer.add_block(PendingBlock::new(vec![1u8; 32]));
430
431        let reader = Reader::from_bytes(writer.to_bytes().unwrap()).unwrap();
432        let (status, _) = reader.verify_block_checksum(0).unwrap();
433        assert_eq!(status, ChecksumStatus::Absent);
434    }
435
436    #[test]
437    fn compressed_blocks_round_trip_through_every_method() {
438        for compression in crate::compression::available() {
439            let payload: Vec<u8> = (0..4096u32).map(|i| (i % 17) as u8).collect();
440            let mut writer = Writer::from_document(tree("a: 1\n"));
441            writer.add_block(PendingBlock::compressed(payload.clone(), compression));
442
443            let reader = Reader::from_bytes(writer.to_bytes().unwrap()).unwrap();
444            assert_eq!(reader.block_compression(0).unwrap(), compression);
445            assert_eq!(&*reader.block_data(0).unwrap(), &payload[..], "{compression:?}");
446            assert_eq!(
447                reader.verify_block_checksum(0).unwrap().0,
448                ChecksumStatus::Valid,
449                "{compression:?}"
450            );
451            // The stored form really is smaller for this redundant payload.
452            assert!(reader.block_raw(0).unwrap().len() < payload.len(), "{compression:?}");
453        }
454    }
455
456    #[test]
457    fn a_written_block_index_is_accepted_on_read_back() {
458        let mut writer = Writer::from_document(tree("a: 1\n"));
459        writer.add_block(PendingBlock::new(vec![1u8; 64]));
460        writer.add_block(PendingBlock::new(vec![2u8; 64]));
461
462        let reader = Reader::from_bytes(writer.to_bytes().unwrap()).unwrap();
463        assert!(
464            reader.layout().used_block_index(),
465            "index rejected: {:?}",
466            reader.layout().index_rejection
467        );
468    }
469
470    #[test]
471    fn no_index_is_written_when_there_are_no_blocks() {
472        // The standard forbids an index in a file with no blocks.
473        let writer = Writer::from_document(tree("a: 1\n"));
474        let reader = Reader::from_bytes(writer.to_bytes().unwrap()).unwrap();
475        assert!(reader.layout().block_index_pos.is_none());
476    }
477
478    #[test]
479    fn the_index_can_be_suppressed() {
480        let options = WriteOptions { write_block_index: false, ..Default::default() };
481        let mut writer = Writer::from_document(tree("a: 1\n")).with_options(options);
482        writer.add_block(PendingBlock::new(vec![3u8; 16]));
483
484        let reader = Reader::from_bytes(writer.to_bytes().unwrap()).unwrap();
485        assert!(reader.layout().block_index_pos.is_none());
486        // ...and the block is still found by skipping along.
487        assert_eq!(reader.block_count(), 1);
488    }
489
490    #[test]
491    fn allocated_size_reserves_room_without_breaking_the_read() {
492        let mut writer = Writer::from_document(tree("a: 1\n"));
493        let data = vec![9u8; 100];
494        writer.add_block(PendingBlock { allocated_size: 4096, ..PendingBlock::new(data.clone()) });
495        writer.add_block(PendingBlock::new(vec![8u8; 10]));
496
497        let reader = Reader::from_bytes(writer.to_bytes().unwrap()).unwrap();
498        assert_eq!(reader.block(0).unwrap().header.allocated_size, 4096);
499        assert_eq!(reader.block(0).unwrap().header.used_size, 100);
500        // The second block must be found past the first's reserved space.
501        assert_eq!(&*reader.block_data(0).unwrap(), &data[..]);
502        assert_eq!(&*reader.block_data(1).unwrap(), &[8u8; 10][..]);
503        assert!(reader.layout().used_block_index());
504    }
505
506    #[test]
507    fn tree_padding_does_not_confuse_the_reader() {
508        let options = WriteOptions { tree_padding: 512, ..Default::default() };
509        let mut writer = Writer::from_document(tree("a: 1\n")).with_options(options);
510        writer.add_block(PendingBlock::new(vec![5u8; 32]));
511
512        let reader = Reader::from_bytes(writer.to_bytes().unwrap()).unwrap();
513        assert_eq!(reader.block_count(), 1);
514        assert_eq!(&*reader.block_data(0).unwrap(), &[5u8; 32][..]);
515        assert!(reader.tree().unwrap().is_some());
516    }
517
518    #[test]
519    fn a_file_with_no_tree_is_valid() {
520        // Exploded form: header straight into blocks.
521        let mut writer = Writer::new();
522        writer.add_block(PendingBlock::new(b"just data".to_vec()));
523
524        let reader = Reader::from_bytes(writer.to_bytes().unwrap()).unwrap();
525        assert!(reader.tree().unwrap().is_none());
526        assert_eq!(&*reader.block_data(0).unwrap(), b"just data");
527    }
528
529    #[test]
530    fn directives_are_supplied_when_the_tree_lacks_them() {
531        // A document built in memory has no directives; the writer must add
532        // them, since the format requires them.
533        let mut doc = Document::new();
534        let k = doc.add_scalar("foo");
535        let v = doc.add_scalar("1");
536        let root = doc.add_mapping(vec![(k, v)]);
537        doc.node_mut(root).tag = Some(Tag::parse("tag:stsci.edu:asdf/core/asdf-1.1.0"));
538        doc.set_root(root);
539
540        let bytes = Writer::from_document(doc).to_bytes().unwrap();
541        let text = String::from_utf8(bytes.clone()).unwrap();
542        assert!(text.contains("%YAML 1.1\n"), "{text}");
543        assert!(text.contains("%TAG ! tag:stsci.edu:asdf/\n"), "{text}");
544        assert!(text.contains("--- !core/asdf-1.1.0\n"), "{text}");
545
546        let reader = Reader::from_bytes(bytes).unwrap();
547        assert!(reader.tree().unwrap().is_some());
548    }
549
550    #[test]
551    fn writes_to_a_path() {
552        let dir = std::env::temp_dir().join(format!("asdf-writer-{}", std::process::id()));
553        std::fs::create_dir_all(&dir).unwrap();
554        let path = dir.join("out.asdf");
555
556        let mut writer = Writer::from_document(tree("a: 1\n"));
557        writer.add_block(PendingBlock::new(b"payload".to_vec()));
558        writer.write_to_path(&path).unwrap();
559
560        let reader = Reader::open(&path).unwrap();
561        assert_eq!(&*reader.block_data(0).unwrap(), b"payload");
562        let _ = std::fs::remove_dir_all(&dir);
563    }
564}