1use 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#[derive(Clone, Debug)]
21pub struct PendingBlock {
22 pub data: Vec<u8>,
25 pub compression: Compression,
27 pub allocated_size: u64,
31 pub already_compressed: bool,
37 pub uncompressed_size: u64,
41}
42
43impl PendingBlock {
44 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 pub fn compressed(data: Vec<u8>, compression: Compression) -> Self {
57 Self { compression, ..Self::new(data) }
58 }
59
60 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#[derive(Clone, Debug)]
71pub struct WriteOptions {
72 pub format_version: String,
74 pub standard_version: String,
76 pub write_block_index: bool,
78 pub write_checksums: bool,
80 pub emit: EmitOptions,
82 pub tree_padding: usize,
85 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#[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 pub fn new() -> Self {
126 Self { document: None, blocks: Vec::new(), options: WriteOptions::default() }
127 }
128
129 pub fn from_document(document: Document) -> Self {
131 Self { document: Some(document), blocks: Vec::new(), options: WriteOptions::default() }
132 }
133
134 pub fn with_options(mut self, options: WriteOptions) -> Self {
136 self.options = options;
137 self
138 }
139
140 pub fn set_document(&mut self, document: Document) {
142 self.document = Some(document);
143 }
144
145 pub fn document(&self) -> Option<&Document> {
147 self.document.as_ref()
148 }
149
150 pub fn document_mut(&mut self) -> Option<&mut Document> {
152 self.document.as_mut()
153 }
154
155 pub fn add_block(&mut self, block: PendingBlock) -> usize {
159 self.blocks.push(block);
160 self.blocks.len() - 1
161 }
162
163 pub fn block_count(&self) -> usize {
165 self.blocks.len()
166 }
167
168 pub fn to_bytes(&self) -> Result<Vec<u8>> {
170 let mut out = Vec::new();
171
172 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 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 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 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 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 fn write_block(&self, out: &mut Vec<u8>, block: &PendingBlock) -> Result<()> {
234 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 header.checksum = md5_of(&stored);
258 }
259
260 header.write(out);
261 out.extend_from_slice(&stored);
262
263 let padding = allocated_size - used_size;
266 out.resize(out.len() + padding as usize, 0);
267 Ok(())
268 }
269
270 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 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 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 #[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 #[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 assert_eq!(reader.block_raw(0).unwrap(), &compressed[..]);
392 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 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 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 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 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 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 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}