Skip to main content

munin_msbuild/
index.rs

1// Copyright (c) Michael Grier
2
3//! Seekable indexed data model over a `.binlog` file.
4//!
5//! [`BinlogIndex`] is built via a single-pass read of the decompressed binlog
6//! stream. After construction, events can be accessed by sequential index or
7//! filtered by record kind, project, target, or task — without re-reading the
8//! file. Individual events are deserialized on demand from stored payloads,
9//! keeping memory usage proportional to the compressed event data rather than
10//! the fully expanded object graph.
11
12use std::io::{Cursor, Read, Write};
13
14use crate::{
15    context::{BuildEventContext, read_build_event_context},
16    error::MuninError,
17    field_flags::BuildEventArgsFieldFlags,
18    header::{BinlogHeader, open_binlog},
19    jsonlog::schema::{JsonlogEventBody, JsonlogFile, MUNIN_JSONLOG_VERSION},
20    nvl_table::{NameValueListTable, NameValuePair},
21    primitives::{read_7bit_count, read_7bit_int},
22    reader::{ArchiveEntry, BinlogEvent, dispatch_event},
23    record_kind::BinaryLogRecordKind,
24    string_table::StringTable,
25    writers::{WriteContext, write_7bit_int as w_7bit},
26};
27
28// ---------------------------------------------------------------------------
29// EventMeta — lightweight per-event metadata captured during first-pass
30// ---------------------------------------------------------------------------
31
32/// Lightweight metadata about one event in the index.
33///
34/// Captured during the first-pass read without fully deserializing the event
35/// payload. Sufficient for filtering and navigation.
36#[derive(Debug, Clone)]
37pub struct EventMeta {
38    /// Record kind discriminant.
39    pub record_kind: BinaryLogRecordKind,
40
41    /// Byte offset of this record's kind byte in the decompressed stream.
42    pub byte_offset: u64,
43
44    /// Byte length of the record payload (excluding kind and length prefix).
45    pub payload_len: usize,
46
47    /// `BuildEventContext` extracted from the common fields prefix, if present.
48    pub context: Option<BuildEventContext>,
49}
50
51// ---------------------------------------------------------------------------
52// IndexEntry — metadata + raw payload for deferred deserialization
53// ---------------------------------------------------------------------------
54
55/// An indexed entry: metadata plus the stored raw payload.
56///
57/// The payload bytes are exactly the record payload (after the record kind
58/// and length prefix). They can be deserialized on demand given the string
59/// table and NVL table captured during the same first pass.
60#[derive(Debug, Clone)]
61struct IndexEntry {
62    meta: EventMeta,
63    payload: Vec<u8>,
64}
65
66// ---------------------------------------------------------------------------
67// BinlogIndex — seekable indexed data model
68// ---------------------------------------------------------------------------
69
70/// Seekable indexed data model over a `.binlog` file.
71///
72/// Built by reading the entire decompressed stream once. Each event record's
73/// raw payload is stored alongside lightweight metadata ([`EventMeta`]).
74/// Events are deserialized on demand via [`get`](Self::get), avoiding the
75/// cost of expanding the full object graph for events that are never accessed.
76///
77/// The string table, name-value-list table, and embedded archives captured
78/// during the first pass are retained for use by on-demand deserialization
79/// and by callers who need string resolution.
80#[derive(Debug)]
81pub struct BinlogIndex {
82    header: BinlogHeader,
83    strings: StringTable,
84    nvl_table: NameValueListTable,
85    archives: Vec<Vec<u8>>,
86    entries: Vec<IndexEntry>,
87}
88
89impl BinlogIndex {
90    // -- Construction -------------------------------------------------------
91
92    /// Build an index by reading all records from the given binlog stream.
93    ///
94    /// This performs a single decompression pass over the entire file,
95    /// ingesting auxiliary records (strings, NVL entries, archives) and
96    /// storing each event record's metadata and raw payload.
97    pub fn open(reader: impl Read) -> Result<Self, MuninError> {
98        let (header, mut gz_reader) = open_binlog(reader)?;
99        let version = header.file_format_version;
100
101        let mut strings = StringTable::new();
102        let mut nvl_table = NameValueListTable::new();
103        let mut archives = Vec::new();
104        let mut entries = Vec::new();
105
106        // Track the byte offset in the decompressed stream. We count bytes
107        // consumed for record-kind, record-length, and payload.
108        let mut offset: u64 = 8; // header is 8 bytes (two i32 LE)
109
110        loop {
111            let kind_start = offset;
112
113            // Record kind: 7-bit variable-length encoded i32.
114            let (kind_raw, kind_bytes) = read_7bit_int_counted(&mut gz_reader)?;
115            offset += kind_bytes;
116
117            if kind_raw == BinaryLogRecordKind::EndOfFile as i32 {
118                break;
119            }
120
121            // Record length (bytes of payload). Always present for v18+.
122            let (record_length, len_bytes) = read_7bit_int_counted(&mut gz_reader)?;
123            if record_length < 0 {
124                return Err(MuninError::InvalidFormat(format!(
125                    "negative record length: {record_length}"
126                )));
127            }
128            let record_length = record_length as usize;
129            if record_length > crate::primitives::MAX_BINLOG_FIELD_LEN {
130                return Err(MuninError::InvalidFormat(format!(
131                    "record length too large: {record_length} (max {})",
132                    crate::primitives::MAX_BINLOG_FIELD_LEN
133                )));
134            }
135            offset += len_bytes;
136
137            // Read the full payload.
138            let mut payload = vec![0u8; record_length];
139            gz_reader.read_exact(&mut payload)?;
140            offset += record_length as u64;
141
142            let kind = BinaryLogRecordKind::from_raw(kind_raw);
143
144            match kind {
145                Some(BinaryLogRecordKind::String) => {
146                    let s = String::from_utf8(payload).map_err(|_| MuninError::InvalidUtf8)?;
147                    strings.add(s);
148                }
149
150                Some(BinaryLogRecordKind::NameValueList) => {
151                    let mut cursor = Cursor::new(&payload);
152                    let count = read_7bit_count(&mut cursor, "name-value list count")?;
153                    let mut pairs = Vec::with_capacity(count);
154                    for _ in 0..count {
155                        let key_index = read_7bit_int(&mut cursor)?;
156                        let value_index = read_7bit_int(&mut cursor)?;
157                        pairs.push(NameValuePair {
158                            key_index,
159                            value_index,
160                        });
161                    }
162                    nvl_table.add(pairs);
163                }
164
165                Some(BinaryLogRecordKind::ProjectImportArchive) => {
166                    archives.push(payload);
167                }
168
169                Some(record_kind) if !record_kind.is_auxiliary() => {
170                    // Extract lightweight metadata from the common fields
171                    // prefix without fully deserializing the event.
172                    let context = extract_context(&payload, version);
173
174                    let meta = EventMeta {
175                        record_kind,
176                        byte_offset: kind_start,
177                        payload_len: record_length,
178                        context,
179                    };
180                    entries.push(IndexEntry { meta, payload });
181                }
182
183                // Unknown or other auxiliary — skip.
184                _ => {}
185            }
186        }
187
188        Ok(Self {
189            header,
190            strings,
191            nvl_table,
192            archives,
193            entries,
194        })
195    }
196
197    /// Build an index from a `.jsonlog` document (see
198    /// [`crate::jsonlog`]).
199    ///
200    /// Reconstructs the string and name-value-list tables verbatim from
201    /// the document so that dedup indices are preserved. Each event is
202    /// either base64-decoded (when stored as `payload_b64`) or
203    /// re-encoded via the `events::write_*` functions (when stored as
204    /// `decoded`) so that the resulting payload is byte-equivalent to
205    /// what the binlog reader would have consumed.
206    pub fn open_json(reader: impl Read) -> Result<Self, MuninError> {
207        let file: JsonlogFile = serde_json::from_reader(reader)
208            .map_err(|e| MuninError::InvalidFormat(format!("jsonlog parse: {e}")))?;
209        Self::from_jsonlog(file)
210    }
211
212    /// Build an index from an already-parsed [`JsonlogFile`].
213    pub fn from_jsonlog(file: JsonlogFile) -> Result<Self, MuninError> {
214        if file.munin_jsonlog_version != MUNIN_JSONLOG_VERSION {
215            return Err(MuninError::InvalidFormat(format!(
216                "unsupported jsonlog version {} (expected {})",
217                file.munin_jsonlog_version, MUNIN_JSONLOG_VERSION
218            )));
219        }
220
221        let header: BinlogHeader = file.header.into();
222        let version = header.file_format_version;
223
224        // Rebuild dedup tables verbatim to preserve indices.
225        let mut strings = StringTable::new();
226        for s in file.strings {
227            strings.add(s);
228        }
229
230        let mut nvl_table = NameValueListTable::new();
231        for entry in file.name_value_lists {
232            let pairs = entry
233                .into_iter()
234                .map(|pair| NameValuePair {
235                    key_index: pair[0] as i32,
236                    value_index: pair[1] as i32,
237                })
238                .collect();
239            nvl_table.add(pairs);
240        }
241
242        let archives: Vec<Vec<u8>> = file
243            .archives
244            .into_iter()
245            .map(|a| {
246                use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
247                BASE64
248                    .decode(a.data_b64.as_bytes())
249                    .map_err(|e| MuninError::InvalidFormat(format!("archive base64: {e}")))
250            })
251            .collect::<Result<Vec<_>, _>>()?;
252
253        let mut entries: Vec<IndexEntry> = Vec::with_capacity(file.events.len());
254        let mut ctx = WriteContext::with_tables(version, strings, nvl_table);
255        for ev in file.events {
256            let record_kind = BinaryLogRecordKind::from_name(&ev.kind).ok_or_else(|| {
257                MuninError::InvalidFormat(format!("unknown event kind: {}", ev.kind))
258            })?;
259            let payload = match ev.body {
260                JsonlogEventBody::PayloadB64(s) => {
261                    use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
262                    BASE64
263                        .decode(s.as_bytes())
264                        .map_err(|e| MuninError::InvalidFormat(format!("payload base64: {e}")))?
265                }
266                JsonlogEventBody::Decoded(value) => {
267                    encode_decoded_event(record_kind, value, &mut ctx)?
268                }
269            };
270            let context = extract_context(&payload, version);
271            entries.push(IndexEntry {
272                meta: EventMeta {
273                    record_kind,
274                    byte_offset: ev.byte_offset,
275                    payload_len: payload.len(),
276                    context,
277                },
278                payload,
279            });
280        }
281        let strings = ctx.strings;
282        let nvl_table = ctx.nvl_table;
283
284        Ok(Self {
285            header,
286            strings,
287            nvl_table,
288            archives,
289            entries,
290        })
291    }
292
293    /// Write `self` as a `.binlog` byte stream to `writer`.
294    ///
295    /// Emits the binlog header, then all `String` and `NameValueList`
296    /// aux records (in their stored order), then any
297    /// `ProjectImportArchive` blobs, then every event record in stored
298    /// order, terminated by an `EndOfFile` sentinel. The whole stream
299    /// is gzip-compressed to match the on-disk binlog format.
300    ///
301    /// The output is semantically equivalent to a roundtrip of the
302    /// original binlog — every `meta(i)` and decoded `get(i)` will
303    /// match — but is not guaranteed to be byte-exact, since the
304    /// original interleaving of aux and event records is not
305    /// preserved by [`crate::jsonlog::JsonlogFile`].
306    pub fn write_binlog<W: Write>(&self, writer: W) -> Result<(), MuninError> {
307        use flate2::{Compression, write::GzEncoder};
308
309        let mut gz = GzEncoder::new(writer, Compression::default());
310
311        // Header: file_format_version + min_reader_version, both little-endian i32.
312        gz.write_all(&self.header.file_format_version.to_le_bytes())?;
313        gz.write_all(&self.header.min_reader_version.to_le_bytes())?;
314
315        // All String records first, preserving original indices.
316        for s in self.strings.entries() {
317            let bytes = s.as_bytes();
318            w_7bit(&mut gz, BinaryLogRecordKind::String as i32)?;
319            w_7bit(&mut gz, bytes.len() as i32)?;
320            gz.write_all(bytes)?;
321        }
322
323        // All NameValueList records next.
324        for list in self.nvl_table.entries() {
325            // Pre-encode payload to a buffer to know length.
326            let mut buf = Vec::new();
327            w_7bit(&mut buf, list.len() as i32)?;
328            for pair in list {
329                w_7bit(&mut buf, pair.key_index)?;
330                w_7bit(&mut buf, pair.value_index)?;
331            }
332            w_7bit(&mut gz, BinaryLogRecordKind::NameValueList as i32)?;
333            w_7bit(&mut gz, buf.len() as i32)?;
334            gz.write_all(&buf)?;
335        }
336
337        // ProjectImportArchive blobs.
338        for archive in &self.archives {
339            w_7bit(&mut gz, BinaryLogRecordKind::ProjectImportArchive as i32)?;
340            w_7bit(&mut gz, archive.len() as i32)?;
341            gz.write_all(archive)?;
342        }
343
344        // Event records.
345        for entry in &self.entries {
346            w_7bit(&mut gz, entry.meta.record_kind as i32)?;
347            w_7bit(&mut gz, entry.payload.len() as i32)?;
348            gz.write_all(&entry.payload)?;
349        }
350
351        // Terminating EndOfFile sentinel.
352        w_7bit(&mut gz, BinaryLogRecordKind::EndOfFile as i32)?;
353
354        gz.finish()?;
355        Ok(())
356    }
357
358    /// The parsed binlog file header.
359    pub fn header(&self) -> &BinlogHeader {
360        &self.header
361    }
362
363    /// The string table accumulated during indexing.
364    pub fn strings(&self) -> &StringTable {
365        &self.strings
366    }
367
368    /// Mutable access to the string table, for in-place rewrites by
369    /// [`crate::redact::Redactor`]. Replacing an entry's contents keeps
370    /// every existing string-index reference valid, since indices are
371    /// positional.
372    pub fn strings_mut(&mut self) -> &mut StringTable {
373        &mut self.strings
374    }
375
376    /// The name-value list table accumulated during indexing.
377    pub fn nvl_table(&self) -> &NameValueListTable {
378        &self.nvl_table
379    }
380
381    /// Embedded zip archives from `ProjectImportArchive` records.
382    pub fn archives(&self) -> &[Vec<u8>] {
383        &self.archives
384    }
385
386    /// Number of event records in the index.
387    pub fn len(&self) -> usize {
388        self.entries.len()
389    }
390
391    /// Whether the index contains zero events.
392    pub fn is_empty(&self) -> bool {
393        self.entries.is_empty()
394    }
395
396    /// Metadata for the event at the given sequential index (0-based).
397    ///
398    /// Returns `None` if `index` is out of range.
399    pub fn meta(&self, index: usize) -> Option<&EventMeta> {
400        self.entries.get(index).map(|e| &e.meta)
401    }
402
403    /// Iterator over all event metadata entries.
404    pub fn iter_meta(&self) -> impl Iterator<Item = (usize, &EventMeta)> {
405        self.entries.iter().enumerate().map(|(i, e)| (i, &e.meta))
406    }
407
408    /// Raw stored payload bytes for the event at `index`, or `None` if
409    /// out of range. The slice contains the record payload exactly as
410    /// it appeared after the record kind / length prefix in the
411    /// decompressed binlog stream.
412    ///
413    /// Primarily intended for the jsonlog dumper's `payload_b64`
414    /// fallback and for round-trip integrity checks.
415    pub fn payload_bytes(&self, index: usize) -> Option<&[u8]> {
416        self.entries.get(index).map(|e| e.payload.as_slice())
417    }
418
419    // -- Random-access deserialization --------------------------------------
420
421    /// Deserialize the event at the given sequential index (0-based).
422    ///
423    /// Returns `None` if `index` is out of range. Returns an error if the
424    /// stored payload cannot be deserialized.
425    pub fn get(&self, index: usize) -> Result<Option<BinlogEvent>, MuninError> {
426        let entry = match self.entries.get(index) {
427            Some(e) => e,
428            None => return Ok(None),
429        };
430
431        let mut cursor = Cursor::new(&entry.payload);
432        let event = dispatch_event(
433            &mut cursor,
434            entry.meta.record_kind,
435            &self.strings,
436            &self.nvl_table,
437            self.header.file_format_version,
438        )?;
439        Ok(Some(event))
440    }
441
442    /// Deserialize all events and collect them into a `Vec`.
443    pub fn get_all(&self) -> Result<Vec<BinlogEvent>, MuninError> {
444        let mut events = Vec::with_capacity(self.entries.len());
445        for i in 0..self.entries.len() {
446            if let Some(event) = self.get(i)? {
447                events.push(event);
448            }
449        }
450        Ok(events)
451    }
452
453    // -- Query / filter API -------------------------------------------------
454
455    /// Indices of events matching the given record kind.
456    pub fn indices_by_kind(&self, kind: BinaryLogRecordKind) -> Vec<usize> {
457        self.entries
458            .iter()
459            .enumerate()
460            .filter(|(_, e)| e.meta.record_kind == kind)
461            .map(|(i, _)| i)
462            .collect()
463    }
464
465    /// Indices of events whose `BuildEventContext` has the given
466    /// `project_context_id`.
467    pub fn indices_by_project_context(&self, project_context_id: i32) -> Vec<usize> {
468        self.entries
469            .iter()
470            .enumerate()
471            .filter(|(_, e)| {
472                e.meta
473                    .context
474                    .is_some_and(|c| c.project_context_id == project_context_id)
475            })
476            .map(|(i, _)| i)
477            .collect()
478    }
479
480    /// Indices of events whose `BuildEventContext` has the given `target_id`.
481    pub fn indices_by_target_id(&self, target_id: i32) -> Vec<usize> {
482        self.entries
483            .iter()
484            .enumerate()
485            .filter(|(_, e)| e.meta.context.is_some_and(|c| c.target_id == target_id))
486            .map(|(i, _)| i)
487            .collect()
488    }
489
490    /// Indices of events whose `BuildEventContext` has the given `task_id`.
491    pub fn indices_by_task_id(&self, task_id: i32) -> Vec<usize> {
492        self.entries
493            .iter()
494            .enumerate()
495            .filter(|(_, e)| e.meta.context.is_some_and(|c| c.task_id == task_id))
496            .map(|(i, _)| i)
497            .collect()
498    }
499
500    /// A combined filter: returns indices of events matching ALL of the
501    /// specified criteria. `None` fields are ignored (wildcard).
502    pub fn query(
503        &self,
504        kind: Option<BinaryLogRecordKind>,
505        project_context_id: Option<i32>,
506        target_id: Option<i32>,
507        task_id: Option<i32>,
508    ) -> Vec<usize> {
509        self.entries
510            .iter()
511            .enumerate()
512            .filter(|(_, e)| {
513                if let Some(k) = kind
514                    && e.meta.record_kind != k
515                {
516                    return false;
517                }
518                if let Some(pid) = project_context_id
519                    && e.meta.context.is_none_or(|c| c.project_context_id != pid)
520                {
521                    return false;
522                }
523                if let Some(tid) = target_id
524                    && e.meta.context.is_none_or(|c| c.target_id != tid)
525                {
526                    return false;
527                }
528                if let Some(tsk) = task_id
529                    && e.meta.context.is_none_or(|c| c.task_id != tsk)
530                {
531                    return false;
532                }
533                true
534            })
535            .map(|(i, _)| i)
536            .collect()
537    }
538
539    /// Extract all files from embedded `ProjectImportArchive` zip archives.
540    pub fn extract_archives(&self) -> Result<Vec<ArchiveEntry>, MuninError> {
541        // Re-use the same extraction logic as BinlogReader.
542        let mut entries = Vec::new();
543        for archive_bytes in &self.archives {
544            let cursor = Cursor::new(archive_bytes);
545            let mut archive = zip::ZipArchive::new(cursor).map_err(|e| {
546                MuninError::InvalidFormat(format!("invalid ProjectImportArchive zip: {e}"))
547            })?;
548            for i in 0..archive.len() {
549                let mut file = archive.by_index(i).map_err(|e| {
550                    MuninError::InvalidFormat(format!("cannot read zip entry {i}: {e}"))
551                })?;
552                if file.is_dir() {
553                    continue;
554                }
555                let path = file.name().to_string();
556                let mut contents = String::new();
557                if file.read_to_string(&mut contents).is_ok() {
558                    entries.push(ArchiveEntry { path, contents });
559                }
560                // Non-UTF-8 binary — skip silently.
561            }
562        }
563        Ok(entries)
564    }
565}
566
567// ---------------------------------------------------------------------------
568// Helpers
569// ---------------------------------------------------------------------------
570
571/// Read a 7-bit variable-length encoded `i32`, also returning the number of
572/// bytes consumed.
573fn read_7bit_int_counted(reader: &mut impl Read) -> Result<(i32, u64), MuninError> {
574    let mut result: u32 = 0;
575    let mut shift: u32 = 0;
576    let mut buf = [0u8; 1];
577    let mut count: u64 = 0;
578
579    for _ in 0..5 {
580        reader.read_exact(&mut buf)?;
581        count += 1;
582        let byte = buf[0];
583        result |= ((byte & 0x7F) as u32) << shift;
584        if byte & 0x80 == 0 {
585            return Ok((result as i32, count));
586        }
587        shift += 7;
588    }
589
590    Err(MuninError::OverlongVarInt)
591}
592
593/// Extract the `BuildEventContext` from the common fields prefix of an event
594/// payload, without fully deserializing the event.
595///
596/// Returns `None` if the flags do not include `BUILD_EVENT_CONTEXT` or if
597/// partial parsing fails (in which case we gracefully degrade to no context).
598fn extract_context(payload: &[u8], file_format_version: i32) -> Option<BuildEventContext> {
599    let mut cursor = Cursor::new(payload);
600
601    // Read the BuildEventArgsFieldFlags bitmask.
602    let flags_raw = read_7bit_int(&mut cursor).ok()? as u32;
603    let flags = BuildEventArgsFieldFlags::from_raw(flags_raw);
604
605    // Skip the MESSAGE field if present (it's a dedup string index = one 7-bit int).
606    if flags.contains(BuildEventArgsFieldFlags::MESSAGE) {
607        let _ = read_7bit_int(&mut cursor).ok()?;
608    }
609
610    // Read the BuildEventContext if present.
611    if flags.contains(BuildEventArgsFieldFlags::BUILD_EVENT_CONTEXT) {
612        read_build_event_context(&mut cursor, file_format_version).ok()
613    } else {
614        None
615    }
616}
617
618#[cfg(test)]
619mod tests;
620
621// ---------------------------------------------------------------------------
622// Decoded-event re-encoder (jsonlog → binlog payload bytes)
623// ---------------------------------------------------------------------------
624
625/// Re-encode a decoded event (deserialized as a `serde_json::Value`) into
626/// the byte sequence the binlog reader for the same record kind would
627/// consume. Used by [`BinlogIndex::open_json`].
628fn encode_decoded_event(
629    kind: BinaryLogRecordKind,
630    value: serde_json::Value,
631    ctx: &mut WriteContext,
632) -> Result<Vec<u8>, MuninError> {
633    use crate::events as ev;
634    let mut buf = Vec::new();
635    macro_rules! enc {
636        ($Ty:ty, $write:path) => {{
637            let parsed: $Ty = serde_json::from_value(value)
638                .map_err(|e| MuninError::InvalidFormat(format!("decoded event: {e}")))?;
639            $write(&mut buf, ctx, &parsed)
640                .map_err(|e| MuninError::InvalidFormat(format!("encode event: {e}")))?;
641        }};
642    }
643    match kind {
644        BinaryLogRecordKind::BuildStarted => enc!(ev::BuildStartedEvent, ev::write_build_started),
645        BinaryLogRecordKind::BuildFinished => {
646            enc!(ev::BuildFinishedEvent, ev::write_build_finished)
647        }
648        BinaryLogRecordKind::ProjectStarted => {
649            enc!(ev::ProjectStartedEvent, ev::write_project_started)
650        }
651        BinaryLogRecordKind::ProjectFinished => {
652            enc!(ev::ProjectFinishedEvent, ev::write_project_finished)
653        }
654        BinaryLogRecordKind::TargetStarted => {
655            enc!(ev::TargetStartedEvent, ev::write_target_started)
656        }
657        BinaryLogRecordKind::TargetFinished => {
658            enc!(ev::TargetFinishedEvent, ev::write_target_finished)
659        }
660        BinaryLogRecordKind::TargetSkipped => {
661            enc!(ev::TargetSkippedEvent, ev::write_target_skipped)
662        }
663        BinaryLogRecordKind::TaskStarted => enc!(ev::TaskStartedEvent, ev::write_task_started),
664        BinaryLogRecordKind::TaskFinished => enc!(ev::TaskFinishedEvent, ev::write_task_finished),
665        BinaryLogRecordKind::TaskCommandLine => {
666            enc!(ev::TaskCommandLineEvent, ev::write_task_command_line)
667        }
668        BinaryLogRecordKind::TaskParameter => {
669            enc!(ev::TaskParameterEvent, ev::write_task_parameter)
670        }
671        BinaryLogRecordKind::Error => enc!(ev::BuildErrorEvent, ev::write_build_error),
672        BinaryLogRecordKind::Warning => enc!(ev::BuildWarningEvent, ev::write_build_warning),
673        BinaryLogRecordKind::Message => enc!(ev::BuildMessageEvent, ev::write_build_message),
674        BinaryLogRecordKind::CriticalBuildMessage => {
675            enc!(
676                ev::CriticalBuildMessageEvent,
677                ev::write_critical_build_message
678            )
679        }
680        BinaryLogRecordKind::ProjectEvaluationStarted => enc!(
681            ev::ProjectEvaluationStartedEvent,
682            ev::write_project_evaluation_started
683        ),
684        BinaryLogRecordKind::ProjectEvaluationFinished => enc!(
685            ev::ProjectEvaluationFinishedEvent,
686            ev::write_project_evaluation_finished
687        ),
688        BinaryLogRecordKind::PropertyReassignment => enc!(
689            ev::PropertyReassignmentEvent,
690            ev::write_property_reassignment
691        ),
692        BinaryLogRecordKind::UninitializedPropertyRead => enc!(
693            ev::UninitializedPropertyReadEvent,
694            ev::write_uninitialized_property_read
695        ),
696        BinaryLogRecordKind::PropertyInitialValueSet => enc!(
697            ev::PropertyInitialValueSetEvent,
698            ev::write_property_initial_value_set
699        ),
700        BinaryLogRecordKind::EnvironmentVariableRead => enc!(
701            ev::EnvironmentVariableReadEvent,
702            ev::write_environment_variable_read
703        ),
704        BinaryLogRecordKind::ResponseFileUsed => {
705            enc!(ev::ResponseFileUsedEvent, ev::write_response_file_used)
706        }
707        BinaryLogRecordKind::AssemblyLoad => enc!(ev::AssemblyLoadEvent, ev::write_assembly_load),
708        BinaryLogRecordKind::ProjectImported => {
709            enc!(ev::ProjectImportedEvent, ev::write_project_imported)
710        }
711        BinaryLogRecordKind::BuildCheckMessage => {
712            enc!(ev::BuildCheckMessageEvent, ev::write_build_message)
713        }
714        BinaryLogRecordKind::BuildCheckWarning => {
715            enc!(ev::BuildCheckWarningEvent, ev::write_build_warning)
716        }
717        BinaryLogRecordKind::BuildCheckError => {
718            enc!(ev::BuildCheckErrorEvent, ev::write_build_error)
719        }
720        BinaryLogRecordKind::BuildCheckTracing => {
721            enc!(ev::BuildCheckTracingEvent, ev::write_build_check_tracing)
722        }
723        BinaryLogRecordKind::BuildCheckAcquisition => enc!(
724            ev::BuildCheckAcquisitionEvent,
725            ev::write_build_check_acquisition
726        ),
727        BinaryLogRecordKind::BuildSubmissionStarted => enc!(
728            ev::BuildSubmissionStartedEvent,
729            ev::write_build_submission_started
730        ),
731        BinaryLogRecordKind::BuildCanceled => {
732            enc!(ev::BuildCanceledEvent, ev::write_build_canceled)
733        }
734        BinaryLogRecordKind::EndOfFile
735        | BinaryLogRecordKind::String
736        | BinaryLogRecordKind::NameValueList
737        | BinaryLogRecordKind::ProjectImportArchive => {
738            return Err(MuninError::InvalidFormat(format!(
739                "cannot encode auxiliary record kind: {:?}",
740                kind
741            )));
742        }
743    }
744    Ok(buf)
745}