Skip to main content

asdf_core/
events.rs

1//! The low-level event stream: what a file contains, in the order it appears.
2//!
3//! This is the engine behind libasdf's event-based parser and behind the
4//! `asdf events` command. Rather than building a tree, it reports what the
5//! file holds: the version headers, any comments, the block index, the
6//! tree's extent, optionally the YAML events inside it, then each block,
7//! then the end.
8//!
9//! The order is upstream's, which is not quite the file's own: the block
10//! index is reported *before* the tree, because it is found by reading back
11//! from the end of the file and knowing it early is what lets the blocks be
12//! located without a scan.
13
14use asdf_yaml::YamlEvent;
15
16use crate::error::Result;
17use crate::layout::{BlockLocation, Layout, scan};
18
19/// One event from the stream.
20#[derive(Clone, Debug)]
21pub enum Event {
22    /// The `#ASDF` line's version.
23    AsdfVersion(String),
24    /// The `#ASDF_STANDARD` line's version.
25    StandardVersion(String),
26    /// A comment line, without its leading `#`.
27    Comment(String),
28    /// The offsets listed in the file's block index.
29    ///
30    /// Reported as the file states them, whether or not they check out;
31    /// judging the index is [`Layout`]'s job, not the stream's.
32    BlockIndex(Vec<u64>),
33    /// The YAML tree begins here.
34    TreeStart { start: usize },
35    /// One YAML event from inside the tree.
36    Yaml(YamlEvent),
37    /// The YAML tree ends here.
38    ///
39    /// `text` carries the tree itself when [`EventOptions::buffer_tree`] was
40    /// asked for, which is what `asdf events --cap-tree` prints.
41    TreeEnd { start: usize, end: usize, text: Option<String> },
42    /// A binary block.
43    Block(BlockLocation),
44    /// The end of the file.
45    End,
46}
47
48/// What to include in the stream.
49#[derive(Clone, Copy, Default, Debug)]
50pub struct EventOptions {
51    /// Report the YAML events inside the tree, not just its extent.
52    pub yaml: bool,
53    /// Keep the tree's text, so the end-of-tree event can carry it.
54    pub buffer_tree: bool,
55}
56
57/// The event stream for a file already in memory.
58///
59/// A YAML tree that will not parse yields no [`Event::Yaml`] events rather
60/// than an error: the rest of the file is still worth reporting, and that is
61/// what makes the command useful on a damaged file.
62pub fn events(buf: &[u8], options: EventOptions) -> Result<Vec<Event>> {
63    let layout = scan(buf)?;
64    Ok(events_from(buf, &layout, options))
65}
66
67/// The event stream for a file whose layout has already been scanned.
68pub fn events_from(buf: &[u8], layout: &Layout, options: EventOptions) -> Vec<Event> {
69    let mut out = Vec::new();
70
71    out.push(Event::AsdfVersion(layout.format_version.to_string()));
72    if let Some(standard) = &layout.standard_version {
73        out.push(Event::StandardVersion(standard.to_string()));
74    }
75    for comment in &layout.comments {
76        out.push(Event::Comment(comment.clone()));
77    }
78    if layout.block_index_pos.is_some() && !layout.block_index_offsets.is_empty() {
79        out.push(Event::BlockIndex(layout.block_index_offsets.clone()));
80    }
81    if let Some(tree) = layout.tree.clone() {
82        out.push(Event::TreeStart { start: tree.start });
83        if options.yaml
84            && let Some(text) = layout.tree_str(buf)
85            && let Ok(parsed) = asdf_yaml::scan_events(text)
86        {
87            out.extend(parsed.into_iter().map(Event::Yaml));
88        }
89        let text = options.buffer_tree.then(|| layout.tree_str(buf).map(str::to_string)).flatten();
90        out.push(Event::TreeEnd { start: tree.start, end: tree.end, text });
91    }
92    for block in &layout.blocks {
93        out.push(Event::Block(block.clone()));
94    }
95    out.push(Event::End);
96    out
97}
98
99impl Event {
100    /// The name libasdf reports for this event's type.
101    pub fn type_name(&self) -> &'static str {
102        match self {
103            Event::AsdfVersion(_) => "ASDF_ASDF_VERSION_EVENT",
104            Event::StandardVersion(_) => "ASDF_STANDARD_VERSION_EVENT",
105            Event::Comment(_) => "ASDF_COMMENT_EVENT",
106            Event::BlockIndex(_) => "ASDF_BLOCK_INDEX_EVENT",
107            Event::TreeStart { .. } => "ASDF_TREE_START_EVENT",
108            Event::Yaml(_) => "ASDF_YAML_EVENT",
109            Event::TreeEnd { .. } => "ASDF_TREE_END_EVENT",
110            Event::Block(_) => "ASDF_BLOCK_EVENT",
111            Event::End => "ASDF_END_EVENT",
112        }
113    }
114}
115
116/// The name libfyaml gives a YAML event, which libasdf passes straight
117/// through.
118///
119/// These are the YAML test suite's event notation rather than anything
120/// spelled out in a header, so they are pinned here and checked against
121/// upstream's committed `events` fixtures.
122pub fn yaml_event_name(event: &YamlEvent) -> &'static str {
123    use asdf_yaml::YamlEventKind as K;
124    match event.kind {
125        K::StreamStart => "+STR",
126        K::StreamEnd => "-STR",
127        K::DocumentStart => "+DOC",
128        K::DocumentEnd => "-DOC",
129        K::MappingStart => "+MAP",
130        K::MappingEnd => "-MAP",
131        K::SequenceStart => "+SEQ",
132        K::SequenceEnd => "-SEQ",
133        K::Scalar => "=VAL",
134        K::Alias => "=ALI",
135    }
136}
137
138/// Render an event the way `asdf_event_print` does.
139///
140/// The `verbose` body is upstream's format character for character, which is
141/// what its committed `events` fixtures pin.
142pub fn render_event(event: &Event, verbose: bool) -> String {
143    let mut out = format!("Event: {}\n", event.type_name());
144    if !verbose {
145        return out;
146    }
147
148    match event {
149        Event::AsdfVersion(v) => out.push_str(&format!("  ASDF Version: {v}\n")),
150        Event::StandardVersion(v) => out.push_str(&format!("  Standard Version: {v}\n")),
151        Event::Comment(text) => out.push_str(&format!("  Comment: {text}\n")),
152        Event::BlockIndex(offsets) => {
153            let listed: Vec<String> = offsets.iter().map(u64::to_string).collect();
154            out.push_str(&format!("  Offsets: {}\n", listed.join(", ")));
155        }
156        Event::TreeStart { start } => {
157            out.push_str(&format!("  Tree start position: {start} (0x{start:x})\n"));
158        }
159        Event::TreeEnd { end, text, .. } => {
160            out.push_str(&format!("  Tree end position: {end} (0x{end:x})\n"));
161            if let Some(text) = text {
162                out.push_str(text);
163                out.push('\n');
164            }
165        }
166        Event::Yaml(yaml) => {
167            out.push_str(&format!("  Type: {}\n", yaml_event_name(yaml)));
168            if let Some(tag) = &yaml.tag {
169                out.push_str(&format!("  Tag: {tag}\n"));
170            }
171            if let Some(value) = &yaml.value
172                && !value.is_empty()
173            {
174                out.push_str(&format!("  Value: {value}\n"));
175            }
176        }
177        Event::Block(block) => {
178            let header = &block.header;
179            out.push_str(&format!(
180                "  Header position: {} (0x{:x})\n",
181                block.header_pos, block.header_pos
182            ));
183            out.push_str(&format!(
184                "  Data position: {} (0x{:x})\n",
185                block.data_pos, block.data_pos
186            ));
187            out.push_str(&format!(
188                "  Allocated size: {} (0x{:x})\n",
189                header.allocated_size, header.allocated_size
190            ));
191            out.push_str(&format!(
192                "  Used size: {} (0x{:x})\n",
193                header.used_size, header.used_size
194            ));
195            out.push_str(&format!(
196                "  Data size: {} (0x{:x})\n",
197                header.data_size, header.data_size
198            ));
199            // The field is four bytes, `\0`-padded; upstream prints `%.4s`,
200            // which stops at the first NUL.
201            if header.compression[0] != 0 {
202                let name = header.compression.split(|b| *b == 0).next().unwrap_or(&[]);
203                out.push_str(&format!("  Compression: {}\n", String::from_utf8_lossy(name)));
204            }
205            out.push_str("  Checksum: ");
206            for byte in header.checksum {
207                out.push_str(&format!("{byte:02x}"));
208            }
209            out.push('\n');
210        }
211        Event::End => {}
212    }
213    out
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    fn sample() -> Vec<u8> {
221        let mut buf = Vec::new();
222        buf.extend_from_slice(b"#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n#a note\n");
223        buf.extend_from_slice(b"%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n");
224        buf.extend_from_slice(b"n: 1\nlist: [a]\n");
225        buf.extend_from_slice(b"...\n");
226        buf
227    }
228
229    fn names(options: EventOptions) -> Vec<&'static str> {
230        events(&sample(), options).unwrap().iter().map(Event::type_name).collect()
231    }
232
233    #[test]
234    fn the_stream_follows_upstreams_order() {
235        assert_eq!(
236            names(EventOptions::default()),
237            vec![
238                "ASDF_ASDF_VERSION_EVENT",
239                "ASDF_STANDARD_VERSION_EVENT",
240                "ASDF_COMMENT_EVENT",
241                "ASDF_TREE_START_EVENT",
242                "ASDF_TREE_END_EVENT",
243                "ASDF_END_EVENT",
244            ]
245        );
246    }
247
248    #[test]
249    fn yaml_events_are_opt_in() {
250        let quiet = names(EventOptions::default());
251        assert!(!quiet.contains(&"ASDF_YAML_EVENT"));
252
253        let loud = names(EventOptions { yaml: true, ..Default::default() });
254        // +STR +DOC +MAP =VAL(n) =VAL(1) =VAL(list) +SEQ =VAL(a) -SEQ -MAP
255        // -DOC -STR.
256        assert_eq!(loud.iter().filter(|n| **n == "ASDF_YAML_EVENT").count(), 12);
257    }
258
259    /// `--cap-tree` keeps the tree's text so the end-of-tree event carries
260    /// it; without it nothing is retained.
261    #[test]
262    fn the_tree_text_is_kept_only_when_asked_for() {
263        let buf = sample();
264
265        let quiet = events(&buf, EventOptions::default()).unwrap();
266        assert!(quiet.iter().any(|e| matches!(e, Event::TreeEnd { text: None, .. })));
267
268        let loud = events(&buf, EventOptions { yaml: false, buffer_tree: true }).unwrap();
269        let text = loud
270            .iter()
271            .find_map(|e| match e {
272                Event::TreeEnd { text, .. } => text.clone(),
273                _ => None,
274            })
275            .expect("the tree text");
276        assert!(text.starts_with("%YAML 1.1"), "{text}");
277        assert!(text.contains("list: [a]"), "{text}");
278
279        // And it is rendered under the end-of-tree event, as libasdf prints
280        // it.
281        let rendered: String =
282            loud.iter().map(|e| render_event(e, true)).collect::<Vec<_>>().join("");
283        let end = rendered.find("Tree end position").expect("the end event");
284        assert!(rendered[end..].contains("list: [a]"));
285    }
286
287    #[test]
288    fn comments_lose_their_leading_hash() {
289        let stream = events(&sample(), EventOptions::default()).unwrap();
290        let comment = stream
291            .iter()
292            .find_map(|e| match e {
293                Event::Comment(text) => Some(text.clone()),
294                _ => None,
295            })
296            .expect("a comment event");
297        assert_eq!(comment, "a note");
298    }
299
300    #[test]
301    fn rendering_is_the_header_line_alone_unless_verbose() {
302        let stream = events(&sample(), EventOptions { yaml: true, ..Default::default() }).unwrap();
303        for event in &stream {
304            let terse = render_event(event, false);
305            assert_eq!(terse, format!("Event: {}\n", event.type_name()));
306            assert!(render_event(event, true).starts_with(&terse));
307        }
308    }
309
310    #[test]
311    fn a_damaged_tree_still_reports_the_rest_of_the_file() {
312        let mut buf = Vec::new();
313        buf.extend_from_slice(b"#ASDF 1.0.0\n#ASDF_STANDARD 1.6.0\n");
314        buf.extend_from_slice(b"%YAML 1.1\n--- !core/asdf-1.1.0\n");
315        // A mapping value that never closes.
316        buf.extend_from_slice(b"a: [1, 2\n");
317        buf.extend_from_slice(b"...\n");
318
319        let stream = events(&buf, EventOptions { yaml: true, ..Default::default() }).unwrap();
320        let names: Vec<&str> = stream.iter().map(Event::type_name).collect();
321        assert!(names.contains(&"ASDF_TREE_START_EVENT"));
322        assert!(names.contains(&"ASDF_TREE_END_EVENT"));
323        assert_eq!(names.last(), Some(&"ASDF_END_EVENT"));
324    }
325}