Skip to main content

munin_msbuild/
events.rs

1// Copyright (c) Michael Grier
2
3//! Typed event structs and reader functions for MSBuild binary log events.
4//!
5//! Each event type has:
6//! - A struct holding the decoded fields.
7//! - A `read_*` function that deserializes the event from a stream, given
8//!   the common `BuildEventArgsFields` prefix and the string/NVL tables.
9//!
10//! The deserialization order and field presence logic matches the C# reference
11//! implementation in `BuildEventArgsReader.cs`. Changing the read order or
12//! field semantics is a breaking change.
13
14use std::io::{Read, Write};
15
16use serde::{Deserialize, Serialize};
17
18use crate::{
19    context::{BuildEventContext, read_build_event_context, write_build_event_context},
20    error::MuninError,
21    fields::{BuildEventArgsFields, read_build_event_args_fields, write_build_event_args_fields},
22    nvl_table::NameValueListTable,
23    primitives::{read_7bit_count, read_7bit_int, read_bool},
24    readers::{read_dedup_string, read_optional_string, read_string_dictionary},
25    string_table::StringTable,
26    writers::{
27        WriteContext, write_7bit_int, write_bool, write_dedup_string, write_dotnet_string,
28        write_guid, write_i64_le, write_optional_string, write_string_dictionary,
29    },
30};
31
32// ---------------------------------------------------------------------------
33// Helper: read diagnostic fields (Error, Warning, CriticalBuildMessage, Message)
34// ---------------------------------------------------------------------------
35
36/// Read the 8 diagnostic location fields that are always present (not flag-driven)
37/// for Error, Warning, and CriticalBuildMessage events.
38fn read_diagnostic_fields(
39    reader: &mut impl Read,
40    strings: &StringTable,
41    file_format_version: i32,
42) -> Result<DiagnosticLocation, MuninError> {
43    let subcategory = read_optional_string(reader, strings, file_format_version)?;
44    let code = read_optional_string(reader, strings, file_format_version)?;
45    let file = read_optional_string(reader, strings, file_format_version)?;
46    let project_file = read_optional_string(reader, strings, file_format_version)?;
47    let line_number = read_7bit_int(reader)?;
48    let column_number = read_7bit_int(reader)?;
49    let end_line_number = read_7bit_int(reader)?;
50    let end_column_number = read_7bit_int(reader)?;
51    Ok(DiagnosticLocation {
52        subcategory,
53        code,
54        file,
55        project_file,
56        line_number,
57        column_number,
58        end_line_number,
59        end_column_number,
60    })
61}
62
63/// Location fields for diagnostic events (Error, Warning, CriticalBuildMessage).
64#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
65pub struct DiagnosticLocation {
66    pub subcategory: Option<String>,
67    pub code: Option<String>,
68    pub file: Option<String>,
69    pub project_file: Option<String>,
70    pub line_number: i32,
71    pub column_number: i32,
72    pub end_line_number: i32,
73    pub end_column_number: i32,
74}
75
76// ---------------------------------------------------------------------------
77// MN-11: Build/Project lifecycle events
78// ---------------------------------------------------------------------------
79
80/// `BuildStarted` event — emitted once at the start of the build.
81#[derive(Debug, Clone, Default, Serialize, Deserialize)]
82pub struct BuildStartedEvent {
83    pub fields: BuildEventArgsFields,
84    /// Environment variables at build start (key → value).
85    pub environment: Option<Vec<(String, String)>>,
86}
87
88/// Read a `BuildStarted` event from the stream.
89pub fn read_build_started(
90    reader: &mut impl Read,
91    strings: &StringTable,
92    nvl_table: &NameValueListTable,
93    file_format_version: i32,
94) -> Result<BuildStartedEvent, MuninError> {
95    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
96    let environment = read_string_dictionary(reader, strings, nvl_table, file_format_version)?;
97    Ok(BuildStartedEvent {
98        fields,
99        environment,
100    })
101}
102
103/// `BuildFinished` event — emitted once at the end of the build.
104#[derive(Debug, Clone, Default, Serialize, Deserialize)]
105pub struct BuildFinishedEvent {
106    pub fields: BuildEventArgsFields,
107    pub succeeded: bool,
108}
109
110/// Read a `BuildFinished` event from the stream.
111pub fn read_build_finished(
112    reader: &mut impl Read,
113    strings: &StringTable,
114    file_format_version: i32,
115) -> Result<BuildFinishedEvent, MuninError> {
116    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
117    let succeeded = read_bool(reader)?;
118    Ok(BuildFinishedEvent { fields, succeeded })
119}
120
121/// `ProjectStarted` event.
122#[derive(Debug, Clone, Default, Serialize, Deserialize)]
123pub struct ProjectStartedEvent {
124    pub fields: BuildEventArgsFields,
125    pub parent_context: Option<BuildEventContext>,
126    pub project_file: Option<String>,
127    pub project_id: i32,
128    pub target_names: Option<String>,
129    pub tools_version: Option<String>,
130    pub global_properties: Option<Vec<(String, String)>>,
131    pub property_list: Option<Vec<(String, String)>>,
132    pub item_list: Option<Vec<ItemGroup>>,
133}
134
135/// Read a `ProjectStarted` event from the stream.
136pub fn read_project_started(
137    reader: &mut impl Read,
138    strings: &StringTable,
139    nvl_table: &NameValueListTable,
140    file_format_version: i32,
141) -> Result<ProjectStartedEvent, MuninError> {
142    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
143
144    let parent_context = if read_bool(reader)? {
145        Some(read_build_event_context(reader, file_format_version)?)
146    } else {
147        None
148    };
149
150    let project_file = read_optional_string(reader, strings, file_format_version)?;
151    let project_id = read_7bit_int(reader)?;
152    let target_names = read_dedup_string(reader, strings)?;
153    let tools_version = read_optional_string(reader, strings, file_format_version)?;
154
155    let global_properties = if file_format_version > 6 {
156        // In v18+ global properties are always written. In v7-17 a bool prefix
157        // controls presence.
158        if file_format_version >= 18 || read_bool(reader)? {
159            read_string_dictionary(reader, strings, nvl_table, file_format_version)?
160        } else {
161            None
162        }
163    } else {
164        None
165    };
166
167    let property_list = read_property_list(reader, strings, nvl_table, file_format_version)?;
168    let item_list = read_project_items(reader, strings, nvl_table, file_format_version)?;
169
170    Ok(ProjectStartedEvent {
171        fields,
172        parent_context,
173        project_file,
174        project_id,
175        target_names,
176        tools_version,
177        global_properties,
178        property_list,
179        item_list,
180    })
181}
182
183/// `ProjectFinished` event.
184#[derive(Debug, Clone, Default, Serialize, Deserialize)]
185pub struct ProjectFinishedEvent {
186    pub fields: BuildEventArgsFields,
187    pub project_file: Option<String>,
188    pub succeeded: bool,
189}
190
191/// Read a `ProjectFinished` event from the stream.
192pub fn read_project_finished(
193    reader: &mut impl Read,
194    strings: &StringTable,
195    file_format_version: i32,
196) -> Result<ProjectFinishedEvent, MuninError> {
197    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
198    let project_file = read_optional_string(reader, strings, file_format_version)?;
199    let succeeded = read_bool(reader)?;
200    Ok(ProjectFinishedEvent {
201        fields,
202        project_file,
203        succeeded,
204    })
205}
206
207// ---------------------------------------------------------------------------
208// MN-12: Target lifecycle events
209// ---------------------------------------------------------------------------
210
211/// `TargetStarted` event.
212#[derive(Debug, Clone, Default, Serialize, Deserialize)]
213pub struct TargetStartedEvent {
214    pub fields: BuildEventArgsFields,
215    pub target_name: Option<String>,
216    pub project_file: Option<String>,
217    pub target_file: Option<String>,
218    pub parent_target: Option<String>,
219    /// `TargetBuiltReason` (introduced in format version 4).
220    pub build_reason: i32,
221}
222
223/// Read a `TargetStarted` event from the stream.
224pub fn read_target_started(
225    reader: &mut impl Read,
226    strings: &StringTable,
227    file_format_version: i32,
228) -> Result<TargetStartedEvent, MuninError> {
229    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
230    let target_name = read_optional_string(reader, strings, file_format_version)?;
231    let project_file = read_optional_string(reader, strings, file_format_version)?;
232    let target_file = read_optional_string(reader, strings, file_format_version)?;
233    let parent_target = read_optional_string(reader, strings, file_format_version)?;
234    let build_reason = if file_format_version > 3 {
235        read_7bit_int(reader)?
236    } else {
237        0 // TargetBuiltReason::None
238    };
239    Ok(TargetStartedEvent {
240        fields,
241        target_name,
242        project_file,
243        target_file,
244        parent_target,
245        build_reason,
246    })
247}
248
249/// `TargetFinished` event.
250#[derive(Debug, Clone, Default, Serialize, Deserialize)]
251pub struct TargetFinishedEvent {
252    pub fields: BuildEventArgsFields,
253    pub succeeded: bool,
254    pub project_file: Option<String>,
255    pub target_file: Option<String>,
256    pub target_name: Option<String>,
257    pub target_outputs: Option<Vec<TaskItem>>,
258}
259
260/// Read a `TargetFinished` event from the stream.
261pub fn read_target_finished(
262    reader: &mut impl Read,
263    strings: &StringTable,
264    nvl_table: &NameValueListTable,
265    file_format_version: i32,
266) -> Result<TargetFinishedEvent, MuninError> {
267    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
268    let succeeded = read_bool(reader)?;
269    let project_file = read_optional_string(reader, strings, file_format_version)?;
270    let target_file = read_optional_string(reader, strings, file_format_version)?;
271    let target_name = read_optional_string(reader, strings, file_format_version)?;
272    let target_outputs = read_task_item_list(reader, strings, nvl_table, file_format_version)?;
273    Ok(TargetFinishedEvent {
274        fields,
275        succeeded,
276        project_file,
277        target_file,
278        target_name,
279        target_outputs,
280    })
281}
282
283/// `TargetSkipped` event.
284#[derive(Debug, Clone, Default, Serialize, Deserialize)]
285pub struct TargetSkippedEvent {
286    pub fields: BuildEventArgsFields,
287    pub target_file: Option<String>,
288    pub target_name: Option<String>,
289    pub parent_target: Option<String>,
290    pub condition: Option<String>,
291    pub evaluated_condition: Option<String>,
292    pub originally_succeeded: bool,
293    /// `TargetSkipReason` (raw i32, introduced in format version 14).
294    pub skip_reason: i32,
295    pub build_reason: i32,
296    pub original_build_event_context: Option<BuildEventContext>,
297}
298
299/// Read a `TargetSkipped` event from the stream.
300pub fn read_target_skipped(
301    reader: &mut impl Read,
302    strings: &StringTable,
303    file_format_version: i32,
304) -> Result<TargetSkippedEvent, MuninError> {
305    let fields = read_build_event_args_fields(reader, strings, file_format_version, true)?;
306    let target_file = read_optional_string(reader, strings, file_format_version)?;
307    let target_name = read_optional_string(reader, strings, file_format_version)?;
308    let parent_target = read_optional_string(reader, strings, file_format_version)?;
309
310    let mut condition = None;
311    let mut evaluated_condition = None;
312    let mut originally_succeeded = false;
313    let mut skip_reason = 0; // TargetSkipReason::None
314
315    if file_format_version >= 13 {
316        condition = read_optional_string(reader, strings, file_format_version)?;
317        evaluated_condition = read_optional_string(reader, strings, file_format_version)?;
318        originally_succeeded = read_bool(reader)?;
319
320        // Infer skip reason from available data (matches C# reference).
321        skip_reason = if condition.is_some() {
322            1 // TargetSkipReason::ConditionWasFalse
323        } else if originally_succeeded {
324            2 // TargetSkipReason::PreviouslyBuiltSuccessfully
325        } else {
326            3 // TargetSkipReason::PreviouslyBuiltUnsuccessfully
327        };
328    }
329
330    let build_reason = read_7bit_int(reader)?;
331
332    let mut original_build_event_context = None;
333    if file_format_version >= 14 {
334        skip_reason = read_7bit_int(reader)?;
335        // Optional BuildEventContext (uses a format like BinaryReaderExtensions)
336        if read_bool(reader)? {
337            original_build_event_context =
338                Some(read_build_event_context(reader, file_format_version)?);
339        }
340    }
341
342    Ok(TargetSkippedEvent {
343        fields,
344        target_file,
345        target_name,
346        parent_target,
347        condition,
348        evaluated_condition,
349        originally_succeeded,
350        skip_reason,
351        build_reason,
352        original_build_event_context,
353    })
354}
355
356// ---------------------------------------------------------------------------
357// MN-13: Task lifecycle events
358// ---------------------------------------------------------------------------
359
360/// `TaskStarted` event.
361#[derive(Debug, Clone, Default, Serialize, Deserialize)]
362pub struct TaskStartedEvent {
363    pub fields: BuildEventArgsFields,
364    pub task_name: Option<String>,
365    pub project_file: Option<String>,
366    pub task_file: Option<String>,
367    pub task_assembly_location: Option<String>,
368}
369
370/// Read a `TaskStarted` event from the stream.
371pub fn read_task_started(
372    reader: &mut impl Read,
373    strings: &StringTable,
374    file_format_version: i32,
375) -> Result<TaskStartedEvent, MuninError> {
376    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
377    let task_name = read_optional_string(reader, strings, file_format_version)?;
378    let project_file = read_optional_string(reader, strings, file_format_version)?;
379    let task_file = read_optional_string(reader, strings, file_format_version)?;
380    let task_assembly_location = if file_format_version > 19 {
381        read_optional_string(reader, strings, file_format_version)?
382    } else {
383        None
384    };
385    Ok(TaskStartedEvent {
386        fields,
387        task_name,
388        project_file,
389        task_file,
390        task_assembly_location,
391    })
392}
393
394/// `TaskFinished` event.
395#[derive(Debug, Clone, Default, Serialize, Deserialize)]
396pub struct TaskFinishedEvent {
397    pub fields: BuildEventArgsFields,
398    pub succeeded: bool,
399    pub task_name: Option<String>,
400    pub project_file: Option<String>,
401    pub task_file: Option<String>,
402}
403
404/// Read a `TaskFinished` event from the stream.
405pub fn read_task_finished(
406    reader: &mut impl Read,
407    strings: &StringTable,
408    file_format_version: i32,
409) -> Result<TaskFinishedEvent, MuninError> {
410    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
411    let succeeded = read_bool(reader)?;
412    let task_name = read_optional_string(reader, strings, file_format_version)?;
413    let project_file = read_optional_string(reader, strings, file_format_version)?;
414    let task_file = read_optional_string(reader, strings, file_format_version)?;
415    Ok(TaskFinishedEvent {
416        fields,
417        succeeded,
418        task_name,
419        project_file,
420        task_file,
421    })
422}
423
424/// `TaskCommandLine` event — the command line used to invoke an external tool.
425#[derive(Debug, Clone, Default, Serialize, Deserialize)]
426pub struct TaskCommandLineEvent {
427    pub fields: BuildEventArgsFields,
428    pub command_line: Option<String>,
429    pub task_name: Option<String>,
430}
431
432/// Read a `TaskCommandLine` event from the stream.
433pub fn read_task_command_line(
434    reader: &mut impl Read,
435    strings: &StringTable,
436    file_format_version: i32,
437) -> Result<TaskCommandLineEvent, MuninError> {
438    let fields = read_build_event_args_fields(reader, strings, file_format_version, true)?;
439    let command_line = read_optional_string(reader, strings, file_format_version)?;
440    let task_name = read_optional_string(reader, strings, file_format_version)?;
441    Ok(TaskCommandLineEvent {
442        fields,
443        command_line,
444        task_name,
445    })
446}
447
448/// `TaskParameter` event — input/output parameters of a task.
449#[derive(Debug, Clone, Default, Serialize, Deserialize)]
450pub struct TaskParameterEvent {
451    pub fields: BuildEventArgsFields,
452    /// `TaskParameterMessageKind` (raw i32).
453    pub kind: i32,
454    pub item_type: Option<String>,
455    pub items: Option<Vec<TaskItem>>,
456    pub parameter_name: Option<String>,
457    pub property_name: Option<String>,
458}
459
460/// Read a `TaskParameter` event from the stream.
461pub fn read_task_parameter(
462    reader: &mut impl Read,
463    strings: &StringTable,
464    nvl_table: &NameValueListTable,
465    file_format_version: i32,
466) -> Result<TaskParameterEvent, MuninError> {
467    let fields = read_build_event_args_fields(reader, strings, file_format_version, true)?;
468    let kind = read_7bit_int(reader)?;
469    let item_type = read_dedup_string(reader, strings)?;
470    let items = read_task_item_list(reader, strings, nvl_table, file_format_version)?;
471    let (parameter_name, property_name) = if file_format_version >= 21 {
472        (
473            read_dedup_string(reader, strings)?,
474            read_dedup_string(reader, strings)?,
475        )
476    } else {
477        (None, None)
478    };
479    Ok(TaskParameterEvent {
480        fields,
481        kind,
482        item_type,
483        items,
484        parameter_name,
485        property_name,
486    })
487}
488
489// ---------------------------------------------------------------------------
490// MN-14: Diagnostic events (Error, Warning, Message, CriticalBuildMessage)
491// ---------------------------------------------------------------------------
492
493/// A build error event.
494#[derive(Debug, Clone, Default, Serialize, Deserialize)]
495pub struct BuildErrorEvent {
496    pub fields: BuildEventArgsFields,
497    pub location: DiagnosticLocation,
498}
499
500/// Read a `BuildError` event from the stream.
501pub fn read_build_error(
502    reader: &mut impl Read,
503    strings: &StringTable,
504    file_format_version: i32,
505) -> Result<BuildErrorEvent, MuninError> {
506    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
507    let location = read_diagnostic_fields(reader, strings, file_format_version)?;
508    Ok(BuildErrorEvent { fields, location })
509}
510
511/// A build warning event.
512#[derive(Debug, Clone, Default, Serialize, Deserialize)]
513pub struct BuildWarningEvent {
514    pub fields: BuildEventArgsFields,
515    pub location: DiagnosticLocation,
516}
517
518/// Read a `BuildWarning` event from the stream.
519pub fn read_build_warning(
520    reader: &mut impl Read,
521    strings: &StringTable,
522    file_format_version: i32,
523) -> Result<BuildWarningEvent, MuninError> {
524    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
525    let location = read_diagnostic_fields(reader, strings, file_format_version)?;
526    Ok(BuildWarningEvent { fields, location })
527}
528
529/// A build message event.
530#[derive(Debug, Clone, Default, Serialize, Deserialize)]
531pub struct BuildMessageEvent {
532    pub fields: BuildEventArgsFields,
533}
534
535/// Read a `BuildMessage` event from the stream.
536pub fn read_build_message(
537    reader: &mut impl Read,
538    strings: &StringTable,
539    file_format_version: i32,
540) -> Result<BuildMessageEvent, MuninError> {
541    let fields = read_build_event_args_fields(reader, strings, file_format_version, true)?;
542    Ok(BuildMessageEvent { fields })
543}
544
545/// A critical build message event (same layout as message, different severity).
546#[derive(Debug, Clone, Default, Serialize, Deserialize)]
547pub struct CriticalBuildMessageEvent {
548    pub fields: BuildEventArgsFields,
549}
550
551/// Read a `CriticalBuildMessage` event from the stream.
552pub fn read_critical_build_message(
553    reader: &mut impl Read,
554    strings: &StringTable,
555    file_format_version: i32,
556) -> Result<CriticalBuildMessageEvent, MuninError> {
557    let fields = read_build_event_args_fields(reader, strings, file_format_version, true)?;
558    Ok(CriticalBuildMessageEvent { fields })
559}
560
561// ---------------------------------------------------------------------------
562// MN-15: Evaluation events
563// ---------------------------------------------------------------------------
564
565/// `ProjectEvaluationStarted` event.
566#[derive(Debug, Clone, Default, Serialize, Deserialize)]
567pub struct ProjectEvaluationStartedEvent {
568    pub fields: BuildEventArgsFields,
569    pub project_file: Option<String>,
570}
571
572/// Read a `ProjectEvaluationStarted` event from the stream.
573pub fn read_project_evaluation_started(
574    reader: &mut impl Read,
575    strings: &StringTable,
576    file_format_version: i32,
577) -> Result<ProjectEvaluationStartedEvent, MuninError> {
578    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
579    let project_file = read_dedup_string(reader, strings)?;
580    Ok(ProjectEvaluationStartedEvent {
581        fields,
582        project_file,
583    })
584}
585
586/// `ProjectEvaluationFinished` event.
587#[derive(Debug, Clone, Default, Serialize, Deserialize)]
588pub struct ProjectEvaluationFinishedEvent {
589    pub fields: BuildEventArgsFields,
590    pub project_file: Option<String>,
591    pub global_properties: Option<Vec<(String, String)>>,
592    pub property_list: Option<Vec<(String, String)>>,
593    pub item_list: Option<Vec<ItemGroup>>,
594    pub has_profile_data: bool,
595    pub profile_data: Option<Vec<ProfileEntry>>,
596}
597
598/// A single profiler entry from evaluation profiling data.
599#[derive(Debug, Clone, Default, Serialize, Deserialize)]
600pub struct ProfileEntry {
601    pub location: EvaluationLocation,
602    pub profiled_location: ProfiledLocation,
603}
604
605/// Location data for an evaluation profiler entry.
606#[derive(Debug, Clone, Default, Serialize, Deserialize)]
607pub struct EvaluationLocation {
608    pub element_name: Option<String>,
609    pub description: Option<String>,
610    pub evaluation_description: Option<String>,
611    pub file: Option<String>,
612    pub kind: i32,
613    pub evaluation_pass: i32,
614    pub line: Option<i32>,
615    pub id: i64,
616    pub parent_id: Option<i64>,
617}
618
619/// Timing data for an evaluation profiler entry.
620#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
621pub struct ProfiledLocation {
622    pub number_of_hits: i32,
623    /// Exclusive time in ticks.
624    pub exclusive_time_ticks: i64,
625    /// Inclusive time in ticks.
626    pub inclusive_time_ticks: i64,
627}
628
629/// Read a `ProjectEvaluationFinished` event from the stream.
630pub fn read_project_evaluation_finished(
631    reader: &mut impl Read,
632    strings: &StringTable,
633    nvl_table: &NameValueListTable,
634    file_format_version: i32,
635) -> Result<ProjectEvaluationFinishedEvent, MuninError> {
636    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
637    let project_file = read_dedup_string(reader, strings)?;
638
639    let mut global_properties = None;
640    let mut property_list = None;
641    let mut item_list = None;
642    let mut has_profile_data = false;
643    let mut profile_data = None;
644
645    if file_format_version >= 12 {
646        // In v18+ global properties are always written. In v12-17 a bool prefix.
647        if file_format_version >= 18 || read_bool(reader)? {
648            global_properties =
649                read_string_dictionary(reader, strings, nvl_table, file_format_version)?;
650        }
651
652        property_list = read_property_list(reader, strings, nvl_table, file_format_version)?;
653        item_list = read_project_items(reader, strings, nvl_table, file_format_version)?;
654    }
655
656    // ProfilerResult introduced in version 5.
657    if file_format_version > 4 {
658        has_profile_data = read_bool(reader)?;
659        if has_profile_data {
660            let count = read_7bit_count(reader, "profile entry count")?;
661            let mut entries = Vec::with_capacity(count);
662            for _ in 0..count {
663                let location = read_evaluation_location(reader, strings, file_format_version)?;
664                let profiled = read_profiled_location(reader)?;
665                entries.push(ProfileEntry {
666                    location,
667                    profiled_location: profiled,
668                });
669            }
670            profile_data = Some(entries);
671        }
672    }
673
674    Ok(ProjectEvaluationFinishedEvent {
675        fields,
676        project_file,
677        global_properties,
678        property_list,
679        item_list,
680        has_profile_data,
681        profile_data,
682    })
683}
684
685// ---------------------------------------------------------------------------
686// MN-16: Property event types
687// ---------------------------------------------------------------------------
688
689/// `PropertyReassignment` event — a property value was overwritten.
690#[derive(Debug, Clone, Default, Serialize, Deserialize)]
691pub struct PropertyReassignmentEvent {
692    pub fields: BuildEventArgsFields,
693    pub property_name: Option<String>,
694    pub previous_value: Option<String>,
695    pub new_value: Option<String>,
696    pub location: Option<String>,
697}
698
699/// Read a `PropertyReassignment` event from the stream.
700pub fn read_property_reassignment(
701    reader: &mut impl Read,
702    strings: &StringTable,
703    file_format_version: i32,
704) -> Result<PropertyReassignmentEvent, MuninError> {
705    let fields = read_build_event_args_fields(reader, strings, file_format_version, true)?;
706    let property_name = read_dedup_string(reader, strings)?;
707    let previous_value = read_dedup_string(reader, strings)?;
708    let new_value = read_dedup_string(reader, strings)?;
709    let location = read_dedup_string(reader, strings)?;
710    Ok(PropertyReassignmentEvent {
711        fields,
712        property_name,
713        previous_value,
714        new_value,
715        location,
716    })
717}
718
719/// `UninitializedPropertyRead` event — a property was read before being set.
720#[derive(Debug, Clone, Default, Serialize, Deserialize)]
721pub struct UninitializedPropertyReadEvent {
722    pub fields: BuildEventArgsFields,
723    pub property_name: Option<String>,
724}
725
726/// Read a `UninitializedPropertyRead` event from the stream.
727pub fn read_uninitialized_property_read(
728    reader: &mut impl Read,
729    strings: &StringTable,
730    file_format_version: i32,
731) -> Result<UninitializedPropertyReadEvent, MuninError> {
732    let fields = read_build_event_args_fields(reader, strings, file_format_version, true)?;
733    let property_name = read_dedup_string(reader, strings)?;
734    Ok(UninitializedPropertyReadEvent {
735        fields,
736        property_name,
737    })
738}
739
740/// `PropertyInitialValueSet` event — a property's initial value was set.
741#[derive(Debug, Clone, Default, Serialize, Deserialize)]
742pub struct PropertyInitialValueSetEvent {
743    pub fields: BuildEventArgsFields,
744    pub property_name: Option<String>,
745    pub property_value: Option<String>,
746    pub property_source: Option<String>,
747}
748
749/// Read a `PropertyInitialValueSet` event from the stream.
750pub fn read_property_initial_value_set(
751    reader: &mut impl Read,
752    strings: &StringTable,
753    file_format_version: i32,
754) -> Result<PropertyInitialValueSetEvent, MuninError> {
755    let fields = read_build_event_args_fields(reader, strings, file_format_version, true)?;
756    let property_name = read_dedup_string(reader, strings)?;
757    let property_value = read_dedup_string(reader, strings)?;
758    let property_source = read_dedup_string(reader, strings)?;
759    Ok(PropertyInitialValueSetEvent {
760        fields,
761        property_name,
762        property_value,
763        property_source,
764    })
765}
766
767// ---------------------------------------------------------------------------
768// MN-17: Environment/Response event types
769// ---------------------------------------------------------------------------
770
771/// `EnvironmentVariableRead` event — an environment variable was read during evaluation.
772#[derive(Debug, Clone, Default, Serialize, Deserialize)]
773pub struct EnvironmentVariableReadEvent {
774    pub fields: BuildEventArgsFields,
775    pub environment_variable_name: Option<String>,
776    /// Line number in the project file (format version ≥ 22 only).
777    pub line: i32,
778    /// Column number in the project file (format version ≥ 22 only).
779    pub column: i32,
780    /// File name where the read occurred (format version ≥ 22 only).
781    pub file_name: Option<String>,
782}
783
784/// Read an `EnvironmentVariableRead` event from the stream.
785pub fn read_environment_variable_read(
786    reader: &mut impl Read,
787    strings: &StringTable,
788    file_format_version: i32,
789) -> Result<EnvironmentVariableReadEvent, MuninError> {
790    let fields = read_build_event_args_fields(reader, strings, file_format_version, true)?;
791    let environment_variable_name = read_dedup_string(reader, strings)?;
792    let (line, column, file_name) = if file_format_version >= 22 {
793        let line = read_7bit_int(reader)?;
794        let column = read_7bit_int(reader)?;
795        let file_name = read_dedup_string(reader, strings)?;
796        (line, column, file_name)
797    } else {
798        (0, 0, None)
799    };
800    Ok(EnvironmentVariableReadEvent {
801        fields,
802        environment_variable_name,
803        line,
804        column,
805        file_name,
806    })
807}
808
809/// `ResponseFileUsed` event — a response file was consumed by MSBuild.
810#[derive(Debug, Clone, Default, Serialize, Deserialize)]
811pub struct ResponseFileUsedEvent {
812    pub fields: BuildEventArgsFields,
813    pub response_file_path: Option<String>,
814}
815
816/// Read a `ResponseFileUsed` event from the stream.
817pub fn read_response_file_used(
818    reader: &mut impl Read,
819    strings: &StringTable,
820    file_format_version: i32,
821) -> Result<ResponseFileUsedEvent, MuninError> {
822    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
823    let response_file_path = read_dedup_string(reader, strings)?;
824    Ok(ResponseFileUsedEvent {
825        fields,
826        response_file_path,
827    })
828}
829
830// ---------------------------------------------------------------------------
831// MN-18: Assembly/Import event types
832// ---------------------------------------------------------------------------
833
834/// `AssemblyLoad` event — an assembly was loaded during the build.
835#[derive(Debug, Clone, Default, Serialize, Deserialize)]
836pub struct AssemblyLoadEvent {
837    pub fields: BuildEventArgsFields,
838    /// `AssemblyLoadingContext` (raw i32).
839    pub context: i32,
840    pub loading_initiator: Option<String>,
841    pub assembly_name: Option<String>,
842    pub assembly_path: Option<String>,
843    /// Module version identifier (GUID, 16 bytes).
844    pub mvid: [u8; 16],
845    pub app_domain_name: Option<String>,
846}
847
848/// Read an `AssemblyLoad` event from the stream.
849pub fn read_assembly_load(
850    reader: &mut impl Read,
851    strings: &StringTable,
852    file_format_version: i32,
853) -> Result<AssemblyLoadEvent, MuninError> {
854    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
855    let context = read_7bit_int(reader)?;
856    let loading_initiator = read_dedup_string(reader, strings)?;
857    let assembly_name = read_dedup_string(reader, strings)?;
858    let assembly_path = read_dedup_string(reader, strings)?;
859    let mvid = crate::primitives::read_guid(reader)?;
860    let app_domain_name = read_dedup_string(reader, strings)?;
861    Ok(AssemblyLoadEvent {
862        fields,
863        context,
864        loading_initiator,
865        assembly_name,
866        assembly_path,
867        mvid,
868        app_domain_name,
869    })
870}
871
872/// `ProjectImported` event — a project file was imported.
873#[derive(Debug, Clone, Default, Serialize, Deserialize)]
874pub struct ProjectImportedEvent {
875    pub fields: BuildEventArgsFields,
876    /// Whether the import was ignored (format version > 2 only).
877    pub import_ignored: bool,
878    pub imported_project_file: Option<String>,
879    pub unexpanded_project: Option<String>,
880}
881
882/// Read a `ProjectImported` event from the stream.
883pub fn read_project_imported(
884    reader: &mut impl Read,
885    strings: &StringTable,
886    file_format_version: i32,
887) -> Result<ProjectImportedEvent, MuninError> {
888    let fields = read_build_event_args_fields(reader, strings, file_format_version, true)?;
889    let import_ignored = if file_format_version > 2 {
890        read_bool(reader)?
891    } else {
892        false
893    };
894    let imported_project_file = read_optional_string(reader, strings, file_format_version)?;
895    let unexpanded_project = read_optional_string(reader, strings, file_format_version)?;
896    Ok(ProjectImportedEvent {
897        fields,
898        import_ignored,
899        imported_project_file,
900        unexpanded_project,
901    })
902}
903
904// ---------------------------------------------------------------------------
905// MN-19: BuildCheck event types
906// ---------------------------------------------------------------------------
907
908// BuildCheckMessage, BuildCheckWarning, and BuildCheckError share the same
909// binary layout as BuildMessage, BuildWarning, and BuildError respectively.
910// Use `read_build_message`, `read_build_warning`, and `read_build_error` to
911// read them. The record kind discriminant distinguishes them at the dispatch
912// layer.
913
914/// Type alias: a BuildCheck message has the same structure as a `BuildMessageEvent`.
915pub type BuildCheckMessageEvent = BuildMessageEvent;
916
917/// Type alias: a BuildCheck warning has the same structure as a `BuildWarningEvent`.
918pub type BuildCheckWarningEvent = BuildWarningEvent;
919
920/// Type alias: a BuildCheck error has the same structure as a `BuildErrorEvent`.
921pub type BuildCheckErrorEvent = BuildErrorEvent;
922
923/// `BuildCheckTracing` event — timing data for build check rules.
924#[derive(Debug, Clone, Default, Serialize, Deserialize)]
925pub struct BuildCheckTracingEvent {
926    pub fields: BuildEventArgsFields,
927    /// Raw tracing data: rule name → duration ticks as string.
928    pub tracing_data: Option<Vec<(String, String)>>,
929}
930
931/// Read a `BuildCheckTracing` event from the stream.
932pub fn read_build_check_tracing(
933    reader: &mut impl Read,
934    strings: &StringTable,
935    nvl_table: &NameValueListTable,
936    file_format_version: i32,
937) -> Result<BuildCheckTracingEvent, MuninError> {
938    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
939    let tracing_data = read_string_dictionary(reader, strings, nvl_table, file_format_version)?;
940    Ok(BuildCheckTracingEvent {
941        fields,
942        tracing_data,
943    })
944}
945
946/// `BuildCheckAcquisition` event — a build check rule was acquired.
947#[derive(Debug, Clone, Default, Serialize, Deserialize)]
948pub struct BuildCheckAcquisitionEvent {
949    pub fields: BuildEventArgsFields,
950    pub acquisition_path: String,
951    pub project_path: String,
952}
953
954/// Read a `BuildCheckAcquisition` event from the stream.
955pub fn read_build_check_acquisition(
956    reader: &mut impl Read,
957    strings: &StringTable,
958    file_format_version: i32,
959) -> Result<BuildCheckAcquisitionEvent, MuninError> {
960    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
961    let acquisition_path = crate::primitives::read_dotnet_string(reader)?;
962    let project_path = crate::primitives::read_dotnet_string(reader)?;
963    Ok(BuildCheckAcquisitionEvent {
964        fields,
965        acquisition_path,
966        project_path,
967    })
968}
969
970// ---------------------------------------------------------------------------
971// MN-20: Remaining event types
972// ---------------------------------------------------------------------------
973
974/// `BuildSubmissionStarted` event — a build submission was queued.
975#[derive(Debug, Clone, Default, Serialize, Deserialize)]
976pub struct BuildSubmissionStartedEvent {
977    pub fields: BuildEventArgsFields,
978    pub global_properties: Option<Vec<(String, String)>>,
979    pub entry_projects_full_path: Option<Vec<String>>,
980    pub target_names: Option<Vec<String>>,
981    /// `BuildRequestDataFlags` (raw i32).
982    pub flags: i32,
983    pub submission_id: i32,
984}
985
986/// Read a `BuildSubmissionStarted` event from the stream.
987pub fn read_build_submission_started(
988    reader: &mut impl Read,
989    strings: &StringTable,
990    nvl_table: &NameValueListTable,
991    file_format_version: i32,
992) -> Result<BuildSubmissionStartedEvent, MuninError> {
993    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
994    let global_properties =
995        read_string_dictionary(reader, strings, nvl_table, file_format_version)?;
996    let entry_projects_full_path = read_string_list(reader, strings)?;
997    let target_names = read_string_list(reader, strings)?;
998    let flags = read_7bit_int(reader)?;
999    let submission_id = read_7bit_int(reader)?;
1000    Ok(BuildSubmissionStartedEvent {
1001        fields,
1002        global_properties,
1003        entry_projects_full_path,
1004        target_names,
1005        flags,
1006        submission_id,
1007    })
1008}
1009
1010/// `BuildCanceled` event — the build was canceled.
1011#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1012pub struct BuildCanceledEvent {
1013    pub fields: BuildEventArgsFields,
1014}
1015
1016/// Read a `BuildCanceled` event from the stream.
1017pub fn read_build_canceled(
1018    reader: &mut impl Read,
1019    strings: &StringTable,
1020    file_format_version: i32,
1021) -> Result<BuildCanceledEvent, MuninError> {
1022    let fields = read_build_event_args_fields(reader, strings, file_format_version, false)?;
1023    Ok(BuildCanceledEvent { fields })
1024}
1025
1026// ---------------------------------------------------------------------------
1027// Shared data structures
1028// ---------------------------------------------------------------------------
1029
1030/// A task item (item spec + metadata).
1031#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1032pub struct TaskItem {
1033    pub item_spec: Option<String>,
1034    pub metadata: Option<Vec<(String, String)>>,
1035}
1036
1037/// A group of items with the same type name.
1038#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1039pub struct ItemGroup {
1040    pub item_type: String,
1041    pub items: Vec<TaskItem>,
1042}
1043
1044// ---------------------------------------------------------------------------
1045// Shared item/property readers
1046// ---------------------------------------------------------------------------
1047
1048/// Read a count-prefixed list of deduplicated strings.
1049///
1050/// Returns `None` if the count is zero.
1051fn read_string_list(
1052    reader: &mut impl Read,
1053    strings: &StringTable,
1054) -> Result<Option<Vec<String>>, MuninError> {
1055    let count = read_7bit_count(reader, "string-list count")?;
1056    if count == 0 {
1057        return Ok(None);
1058    }
1059    let mut list = Vec::with_capacity(count);
1060    for _ in 0..count {
1061        if let Some(s) = read_dedup_string(reader, strings)? {
1062            list.push(s);
1063        }
1064    }
1065    if list.is_empty() {
1066        Ok(None)
1067    } else {
1068        Ok(Some(list))
1069    }
1070}
1071
1072/// Read a property list (string dictionary representing properties).
1073fn read_property_list(
1074    reader: &mut impl Read,
1075    strings: &StringTable,
1076    nvl_table: &NameValueListTable,
1077    file_format_version: i32,
1078) -> Result<Option<Vec<(String, String)>>, MuninError> {
1079    read_string_dictionary(reader, strings, nvl_table, file_format_version)
1080}
1081
1082/// Read a single task item (item spec + metadata dictionary).
1083fn read_task_item(
1084    reader: &mut impl Read,
1085    strings: &StringTable,
1086    nvl_table: &NameValueListTable,
1087    file_format_version: i32,
1088) -> Result<TaskItem, MuninError> {
1089    let item_spec = read_dedup_string(reader, strings)?;
1090    let metadata = read_string_dictionary(reader, strings, nvl_table, file_format_version)?;
1091    Ok(TaskItem {
1092        item_spec,
1093        metadata,
1094    })
1095}
1096
1097/// Read a list of task items (count-prefixed).
1098fn read_task_item_list(
1099    reader: &mut impl Read,
1100    strings: &StringTable,
1101    nvl_table: &NameValueListTable,
1102    file_format_version: i32,
1103) -> Result<Option<Vec<TaskItem>>, MuninError> {
1104    let count = read_7bit_count(reader, "task-item count")?;
1105    if count == 0 {
1106        return Ok(None);
1107    }
1108    let mut items = Vec::with_capacity(count);
1109    for _ in 0..count {
1110        items.push(read_task_item(
1111            reader,
1112            strings,
1113            nvl_table,
1114            file_format_version,
1115        )?);
1116    }
1117    Ok(Some(items))
1118}
1119
1120/// Read project items (grouped by item type).
1121///
1122/// The format evolved across versions:
1123/// - Pre-v10: flat list of (item-name-string, task-item) pairs.
1124/// - v10–11: count-prefixed groups of (item-type-dedup, task-item-list).
1125/// - v12+: sequential groups terminated by an empty item-type string.
1126fn read_project_items(
1127    reader: &mut impl Read,
1128    strings: &StringTable,
1129    nvl_table: &NameValueListTable,
1130    file_format_version: i32,
1131) -> Result<Option<Vec<ItemGroup>>, MuninError> {
1132    if file_format_version < 10 {
1133        let count = read_7bit_count(reader, "item count")?;
1134        if count == 0 {
1135            return Ok(None);
1136        }
1137        let mut groups: Vec<ItemGroup> = Vec::new();
1138        let mut group_index: std::collections::HashMap<String, usize> =
1139            std::collections::HashMap::new();
1140        for _ in 0..count {
1141            let item_name = read_dotnet_string_via_reader(reader)?;
1142            let item = read_task_item(reader, strings, nvl_table, file_format_version)?;
1143            // Group by item_name using a map to avoid O(n²) linear scans.
1144            if let Some(&idx) = group_index.get(&item_name) {
1145                groups[idx].items.push(item);
1146            } else {
1147                let idx = groups.len();
1148                group_index.insert(item_name.clone(), idx);
1149                groups.push(ItemGroup {
1150                    item_type: item_name,
1151                    items: vec![item],
1152                });
1153            }
1154        }
1155        Ok(Some(groups))
1156    } else if file_format_version < 12 {
1157        let count = read_7bit_count(reader, "item-group count")?;
1158        if count == 0 {
1159            return Ok(None);
1160        }
1161        let mut groups = Vec::with_capacity(count);
1162        for _ in 0..count {
1163            let item_type = read_dedup_string(reader, strings)?.unwrap_or_default();
1164            let items = read_task_item_list(reader, strings, nvl_table, file_format_version)?
1165                .unwrap_or_default();
1166            groups.push(ItemGroup { item_type, items });
1167        }
1168        if groups.is_empty() {
1169            Ok(None)
1170        } else {
1171            Ok(Some(groups))
1172        }
1173    } else {
1174        // v12+: groups terminated by empty item type.
1175        let mut groups = Vec::new();
1176        loop {
1177            let item_type = read_dedup_string(reader, strings)?.unwrap_or_default();
1178            if item_type.is_empty() {
1179                break;
1180            }
1181            let items = read_task_item_list(reader, strings, nvl_table, file_format_version)?
1182                .unwrap_or_default();
1183            groups.push(ItemGroup { item_type, items });
1184        }
1185        if groups.is_empty() {
1186            Ok(None)
1187        } else {
1188            Ok(Some(groups))
1189        }
1190    }
1191}
1192
1193/// Read a raw .NET string (used for pre-v10 item names).
1194fn read_dotnet_string_via_reader(reader: &mut impl Read) -> Result<String, MuninError> {
1195    crate::primitives::read_dotnet_string(reader)
1196}
1197
1198/// Read an `EvaluationLocation` for profiler data.
1199fn read_evaluation_location(
1200    reader: &mut impl Read,
1201    strings: &StringTable,
1202    file_format_version: i32,
1203) -> Result<EvaluationLocation, MuninError> {
1204    let element_name = read_optional_string(reader, strings, file_format_version)?;
1205    let description = read_optional_string(reader, strings, file_format_version)?;
1206    let evaluation_description = read_optional_string(reader, strings, file_format_version)?;
1207    let file = read_optional_string(reader, strings, file_format_version)?;
1208    let kind = read_7bit_int(reader)?;
1209    let evaluation_pass = read_7bit_int(reader)?;
1210
1211    let line = if read_bool(reader)? {
1212        Some(read_7bit_int(reader)?)
1213    } else {
1214        None
1215    };
1216
1217    // id and parent_id introduced in format version 6.
1218    let (id, parent_id) = if file_format_version > 5 {
1219        let id = crate::primitives::read_i64_le(reader)?;
1220        let parent_id = if read_bool(reader)? {
1221            Some(crate::primitives::read_i64_le(reader)?)
1222        } else {
1223            None
1224        };
1225        (id, parent_id)
1226    } else {
1227        (0, None)
1228    };
1229
1230    Ok(EvaluationLocation {
1231        element_name,
1232        description,
1233        evaluation_description,
1234        file,
1235        kind,
1236        evaluation_pass,
1237        line,
1238        id,
1239        parent_id,
1240    })
1241}
1242
1243/// Read a `ProfiledLocation` (hit count + exclusive/inclusive times).
1244fn read_profiled_location(reader: &mut impl Read) -> Result<ProfiledLocation, MuninError> {
1245    let number_of_hits = read_7bit_int(reader)?;
1246    let exclusive_time_ticks = crate::primitives::read_i64_le(reader)?;
1247    let inclusive_time_ticks = crate::primitives::read_i64_le(reader)?;
1248    Ok(ProfiledLocation {
1249        number_of_hits,
1250        exclusive_time_ticks,
1251        inclusive_time_ticks,
1252    })
1253}
1254
1255// ===========================================================================
1256// WRITERS — every `read_*` above has a paired `write_*` here.
1257// ===========================================================================
1258
1259fn write_diagnostic_fields<W: Write>(
1260    w: &mut W,
1261    ctx: &mut WriteContext,
1262    loc: &DiagnosticLocation,
1263) -> std::io::Result<()> {
1264    write_optional_string(w, ctx, loc.subcategory.as_deref())?;
1265    write_optional_string(w, ctx, loc.code.as_deref())?;
1266    write_optional_string(w, ctx, loc.file.as_deref())?;
1267    write_optional_string(w, ctx, loc.project_file.as_deref())?;
1268    write_7bit_int(w, loc.line_number)?;
1269    write_7bit_int(w, loc.column_number)?;
1270    write_7bit_int(w, loc.end_line_number)?;
1271    write_7bit_int(w, loc.end_column_number)
1272}
1273
1274fn write_string_list<W: Write>(
1275    w: &mut W,
1276    ctx: &mut WriteContext,
1277    list: Option<&[String]>,
1278) -> std::io::Result<()> {
1279    match list {
1280        None => write_7bit_int(w, 0),
1281        Some(items) => {
1282            write_7bit_int(w, items.len() as i32)?;
1283            for s in items {
1284                write_dedup_string(w, ctx, Some(s))?;
1285            }
1286            Ok(())
1287        }
1288    }
1289}
1290
1291fn write_task_item<W: Write>(
1292    w: &mut W,
1293    ctx: &mut WriteContext,
1294    item: &TaskItem,
1295) -> std::io::Result<()> {
1296    write_dedup_string(w, ctx, item.item_spec.as_deref())?;
1297    write_string_dictionary(w, ctx, item.metadata.as_deref())
1298}
1299
1300fn write_task_item_list<W: Write>(
1301    w: &mut W,
1302    ctx: &mut WriteContext,
1303    items: Option<&[TaskItem]>,
1304) -> std::io::Result<()> {
1305    match items {
1306        None => write_7bit_int(w, 0),
1307        Some(items) => {
1308            write_7bit_int(w, items.len() as i32)?;
1309            for it in items {
1310                write_task_item(w, ctx, it)?;
1311            }
1312            Ok(())
1313        }
1314    }
1315}
1316
1317fn write_project_items<W: Write>(
1318    w: &mut W,
1319    ctx: &mut WriteContext,
1320    items: Option<&[ItemGroup]>,
1321) -> std::io::Result<()> {
1322    let v = ctx.file_format_version;
1323    if v < 10 {
1324        let total: usize = items
1325            .map(|g| g.iter().map(|gr| gr.items.len()).sum())
1326            .unwrap_or(0);
1327        write_7bit_int(w, total as i32)?;
1328        if let Some(groups) = items {
1329            for g in groups {
1330                for it in &g.items {
1331                    write_dotnet_string(w, &g.item_type)?;
1332                    write_task_item(w, ctx, it)?;
1333                }
1334            }
1335        }
1336        Ok(())
1337    } else if v < 12 {
1338        match items {
1339            None => write_7bit_int(w, 0),
1340            Some(groups) => {
1341                write_7bit_int(w, groups.len() as i32)?;
1342                for g in groups {
1343                    write_dedup_string(w, ctx, Some(&g.item_type))?;
1344                    write_task_item_list(w, ctx, Some(&g.items))?;
1345                }
1346                Ok(())
1347            }
1348        }
1349    } else {
1350        // v12+: sequential groups terminated by empty item-type sentinel.
1351        if let Some(groups) = items {
1352            for g in groups {
1353                write_dedup_string(w, ctx, Some(&g.item_type))?;
1354                write_task_item_list(w, ctx, Some(&g.items))?;
1355            }
1356        }
1357        // Empty-string sentinel terminates the group sequence. Dedup index
1358        // for empty string is 1.
1359        write_dedup_string(w, ctx, Some(""))
1360    }
1361}
1362
1363fn write_evaluation_location<W: Write>(
1364    w: &mut W,
1365    ctx: &mut WriteContext,
1366    loc: &EvaluationLocation,
1367) -> std::io::Result<()> {
1368    write_optional_string(w, ctx, loc.element_name.as_deref())?;
1369    write_optional_string(w, ctx, loc.description.as_deref())?;
1370    write_optional_string(w, ctx, loc.evaluation_description.as_deref())?;
1371    write_optional_string(w, ctx, loc.file.as_deref())?;
1372    write_7bit_int(w, loc.kind)?;
1373    write_7bit_int(w, loc.evaluation_pass)?;
1374    match loc.line {
1375        Some(line) => {
1376            write_bool(w, true)?;
1377            write_7bit_int(w, line)?;
1378        }
1379        None => write_bool(w, false)?,
1380    }
1381    if ctx.file_format_version > 5 {
1382        write_i64_le(w, loc.id)?;
1383        match loc.parent_id {
1384            Some(pid) => {
1385                write_bool(w, true)?;
1386                write_i64_le(w, pid)?;
1387            }
1388            None => write_bool(w, false)?,
1389        }
1390    }
1391    Ok(())
1392}
1393
1394fn write_profiled_location<W: Write>(w: &mut W, pl: &ProfiledLocation) -> std::io::Result<()> {
1395    write_7bit_int(w, pl.number_of_hits)?;
1396    write_i64_le(w, pl.exclusive_time_ticks)?;
1397    write_i64_le(w, pl.inclusive_time_ticks)
1398}
1399
1400/// Write a `BuildStarted` event.
1401pub fn write_build_started<W: Write>(
1402    w: &mut W,
1403    ctx: &mut WriteContext,
1404    ev: &BuildStartedEvent,
1405) -> std::io::Result<()> {
1406    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1407    write_string_dictionary(w, ctx, ev.environment.as_deref())
1408}
1409
1410/// Write a `BuildFinished` event.
1411pub fn write_build_finished<W: Write>(
1412    w: &mut W,
1413    ctx: &mut WriteContext,
1414    ev: &BuildFinishedEvent,
1415) -> std::io::Result<()> {
1416    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1417    write_bool(w, ev.succeeded)
1418}
1419
1420/// Write a `ProjectStarted` event.
1421pub fn write_project_started<W: Write>(
1422    w: &mut W,
1423    ctx: &mut WriteContext,
1424    ev: &ProjectStartedEvent,
1425) -> std::io::Result<()> {
1426    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1427    match &ev.parent_context {
1428        Some(pc) => {
1429            write_bool(w, true)?;
1430            write_build_event_context(w, pc, ctx.file_format_version)?;
1431        }
1432        None => write_bool(w, false)?,
1433    }
1434    write_optional_string(w, ctx, ev.project_file.as_deref())?;
1435    write_7bit_int(w, ev.project_id)?;
1436    write_dedup_string(w, ctx, ev.target_names.as_deref())?;
1437    write_optional_string(w, ctx, ev.tools_version.as_deref())?;
1438
1439    let v = ctx.file_format_version;
1440    if v > 6 {
1441        if v >= 18 {
1442            write_string_dictionary(w, ctx, ev.global_properties.as_deref())?;
1443        } else {
1444            match &ev.global_properties {
1445                Some(gp) => {
1446                    write_bool(w, true)?;
1447                    write_string_dictionary(w, ctx, Some(gp))?;
1448                }
1449                None => write_bool(w, false)?,
1450            }
1451        }
1452    }
1453    write_string_dictionary(w, ctx, ev.property_list.as_deref())?;
1454    write_project_items(w, ctx, ev.item_list.as_deref())
1455}
1456
1457/// Write a `ProjectFinished` event.
1458pub fn write_project_finished<W: Write>(
1459    w: &mut W,
1460    ctx: &mut WriteContext,
1461    ev: &ProjectFinishedEvent,
1462) -> std::io::Result<()> {
1463    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1464    write_optional_string(w, ctx, ev.project_file.as_deref())?;
1465    write_bool(w, ev.succeeded)
1466}
1467
1468/// Write a `TargetStarted` event.
1469pub fn write_target_started<W: Write>(
1470    w: &mut W,
1471    ctx: &mut WriteContext,
1472    ev: &TargetStartedEvent,
1473) -> std::io::Result<()> {
1474    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1475    write_optional_string(w, ctx, ev.target_name.as_deref())?;
1476    write_optional_string(w, ctx, ev.project_file.as_deref())?;
1477    write_optional_string(w, ctx, ev.target_file.as_deref())?;
1478    write_optional_string(w, ctx, ev.parent_target.as_deref())?;
1479    if ctx.file_format_version > 3 {
1480        write_7bit_int(w, ev.build_reason)?;
1481    }
1482    Ok(())
1483}
1484
1485/// Write a `TargetFinished` event.
1486pub fn write_target_finished<W: Write>(
1487    w: &mut W,
1488    ctx: &mut WriteContext,
1489    ev: &TargetFinishedEvent,
1490) -> std::io::Result<()> {
1491    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1492    write_bool(w, ev.succeeded)?;
1493    write_optional_string(w, ctx, ev.project_file.as_deref())?;
1494    write_optional_string(w, ctx, ev.target_file.as_deref())?;
1495    write_optional_string(w, ctx, ev.target_name.as_deref())?;
1496    write_task_item_list(w, ctx, ev.target_outputs.as_deref())
1497}
1498
1499/// Write a `TargetSkipped` event.
1500pub fn write_target_skipped<W: Write>(
1501    w: &mut W,
1502    ctx: &mut WriteContext,
1503    ev: &TargetSkippedEvent,
1504) -> std::io::Result<()> {
1505    write_build_event_args_fields(w, ctx, &ev.fields, true)?;
1506    write_optional_string(w, ctx, ev.target_file.as_deref())?;
1507    write_optional_string(w, ctx, ev.target_name.as_deref())?;
1508    write_optional_string(w, ctx, ev.parent_target.as_deref())?;
1509
1510    let v = ctx.file_format_version;
1511    if v >= 13 {
1512        write_optional_string(w, ctx, ev.condition.as_deref())?;
1513        write_optional_string(w, ctx, ev.evaluated_condition.as_deref())?;
1514        write_bool(w, ev.originally_succeeded)?;
1515    }
1516    write_7bit_int(w, ev.build_reason)?;
1517    if v >= 14 {
1518        write_7bit_int(w, ev.skip_reason)?;
1519        match &ev.original_build_event_context {
1520            Some(bec) => {
1521                write_bool(w, true)?;
1522                write_build_event_context(w, bec, ctx.file_format_version)?;
1523            }
1524            None => write_bool(w, false)?,
1525        }
1526    }
1527    Ok(())
1528}
1529
1530/// Write a `TaskStarted` event.
1531pub fn write_task_started<W: Write>(
1532    w: &mut W,
1533    ctx: &mut WriteContext,
1534    ev: &TaskStartedEvent,
1535) -> std::io::Result<()> {
1536    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1537    write_optional_string(w, ctx, ev.task_name.as_deref())?;
1538    write_optional_string(w, ctx, ev.project_file.as_deref())?;
1539    write_optional_string(w, ctx, ev.task_file.as_deref())?;
1540    if ctx.file_format_version > 19 {
1541        write_optional_string(w, ctx, ev.task_assembly_location.as_deref())?;
1542    }
1543    Ok(())
1544}
1545
1546/// Write a `TaskFinished` event.
1547pub fn write_task_finished<W: Write>(
1548    w: &mut W,
1549    ctx: &mut WriteContext,
1550    ev: &TaskFinishedEvent,
1551) -> std::io::Result<()> {
1552    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1553    write_bool(w, ev.succeeded)?;
1554    write_optional_string(w, ctx, ev.task_name.as_deref())?;
1555    write_optional_string(w, ctx, ev.project_file.as_deref())?;
1556    write_optional_string(w, ctx, ev.task_file.as_deref())
1557}
1558
1559/// Write a `TaskCommandLine` event.
1560pub fn write_task_command_line<W: Write>(
1561    w: &mut W,
1562    ctx: &mut WriteContext,
1563    ev: &TaskCommandLineEvent,
1564) -> std::io::Result<()> {
1565    write_build_event_args_fields(w, ctx, &ev.fields, true)?;
1566    write_optional_string(w, ctx, ev.command_line.as_deref())?;
1567    write_optional_string(w, ctx, ev.task_name.as_deref())
1568}
1569
1570/// Write a `TaskParameter` event.
1571pub fn write_task_parameter<W: Write>(
1572    w: &mut W,
1573    ctx: &mut WriteContext,
1574    ev: &TaskParameterEvent,
1575) -> std::io::Result<()> {
1576    write_build_event_args_fields(w, ctx, &ev.fields, true)?;
1577    write_7bit_int(w, ev.kind)?;
1578    write_dedup_string(w, ctx, ev.item_type.as_deref())?;
1579    write_task_item_list(w, ctx, ev.items.as_deref())?;
1580    if ctx.file_format_version >= 21 {
1581        write_dedup_string(w, ctx, ev.parameter_name.as_deref())?;
1582        write_dedup_string(w, ctx, ev.property_name.as_deref())?;
1583    }
1584    Ok(())
1585}
1586
1587/// Write a `BuildError` event.
1588pub fn write_build_error<W: Write>(
1589    w: &mut W,
1590    ctx: &mut WriteContext,
1591    ev: &BuildErrorEvent,
1592) -> std::io::Result<()> {
1593    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1594    write_diagnostic_fields(w, ctx, &ev.location)
1595}
1596
1597/// Write a `BuildWarning` event.
1598pub fn write_build_warning<W: Write>(
1599    w: &mut W,
1600    ctx: &mut WriteContext,
1601    ev: &BuildWarningEvent,
1602) -> std::io::Result<()> {
1603    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1604    write_diagnostic_fields(w, ctx, &ev.location)
1605}
1606
1607/// Write a `BuildMessage` event.
1608pub fn write_build_message<W: Write>(
1609    w: &mut W,
1610    ctx: &mut WriteContext,
1611    ev: &BuildMessageEvent,
1612) -> std::io::Result<()> {
1613    write_build_event_args_fields(w, ctx, &ev.fields, true)
1614}
1615
1616/// Write a `CriticalBuildMessage` event.
1617pub fn write_critical_build_message<W: Write>(
1618    w: &mut W,
1619    ctx: &mut WriteContext,
1620    ev: &CriticalBuildMessageEvent,
1621) -> std::io::Result<()> {
1622    write_build_event_args_fields(w, ctx, &ev.fields, true)
1623}
1624
1625/// Write a `ProjectEvaluationStarted` event.
1626pub fn write_project_evaluation_started<W: Write>(
1627    w: &mut W,
1628    ctx: &mut WriteContext,
1629    ev: &ProjectEvaluationStartedEvent,
1630) -> std::io::Result<()> {
1631    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1632    write_dedup_string(w, ctx, ev.project_file.as_deref())
1633}
1634
1635/// Write a `ProjectEvaluationFinished` event.
1636pub fn write_project_evaluation_finished<W: Write>(
1637    w: &mut W,
1638    ctx: &mut WriteContext,
1639    ev: &ProjectEvaluationFinishedEvent,
1640) -> std::io::Result<()> {
1641    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1642    write_dedup_string(w, ctx, ev.project_file.as_deref())?;
1643
1644    let v = ctx.file_format_version;
1645    if v >= 12 {
1646        if v >= 18 {
1647            write_string_dictionary(w, ctx, ev.global_properties.as_deref())?;
1648        } else {
1649            match &ev.global_properties {
1650                Some(gp) => {
1651                    write_bool(w, true)?;
1652                    write_string_dictionary(w, ctx, Some(gp))?;
1653                }
1654                None => write_bool(w, false)?,
1655            }
1656        }
1657        write_string_dictionary(w, ctx, ev.property_list.as_deref())?;
1658        write_project_items(w, ctx, ev.item_list.as_deref())?;
1659    }
1660
1661    if v > 4 {
1662        write_bool(w, ev.has_profile_data)?;
1663        if ev.has_profile_data {
1664            let entries = ev.profile_data.as_deref().unwrap_or(&[]);
1665            write_7bit_int(w, entries.len() as i32)?;
1666            for e in entries {
1667                write_evaluation_location(w, ctx, &e.location)?;
1668                write_profiled_location(w, &e.profiled_location)?;
1669            }
1670        }
1671    }
1672    Ok(())
1673}
1674
1675/// Write a `PropertyReassignment` event.
1676pub fn write_property_reassignment<W: Write>(
1677    w: &mut W,
1678    ctx: &mut WriteContext,
1679    ev: &PropertyReassignmentEvent,
1680) -> std::io::Result<()> {
1681    write_build_event_args_fields(w, ctx, &ev.fields, true)?;
1682    write_dedup_string(w, ctx, ev.property_name.as_deref())?;
1683    write_dedup_string(w, ctx, ev.previous_value.as_deref())?;
1684    write_dedup_string(w, ctx, ev.new_value.as_deref())?;
1685    write_dedup_string(w, ctx, ev.location.as_deref())
1686}
1687
1688/// Write a `UninitializedPropertyRead` event.
1689pub fn write_uninitialized_property_read<W: Write>(
1690    w: &mut W,
1691    ctx: &mut WriteContext,
1692    ev: &UninitializedPropertyReadEvent,
1693) -> std::io::Result<()> {
1694    write_build_event_args_fields(w, ctx, &ev.fields, true)?;
1695    write_dedup_string(w, ctx, ev.property_name.as_deref())
1696}
1697
1698/// Write a `PropertyInitialValueSet` event.
1699pub fn write_property_initial_value_set<W: Write>(
1700    w: &mut W,
1701    ctx: &mut WriteContext,
1702    ev: &PropertyInitialValueSetEvent,
1703) -> std::io::Result<()> {
1704    write_build_event_args_fields(w, ctx, &ev.fields, true)?;
1705    write_dedup_string(w, ctx, ev.property_name.as_deref())?;
1706    write_dedup_string(w, ctx, ev.property_value.as_deref())?;
1707    write_dedup_string(w, ctx, ev.property_source.as_deref())
1708}
1709
1710/// Write an `EnvironmentVariableRead` event.
1711pub fn write_environment_variable_read<W: Write>(
1712    w: &mut W,
1713    ctx: &mut WriteContext,
1714    ev: &EnvironmentVariableReadEvent,
1715) -> std::io::Result<()> {
1716    write_build_event_args_fields(w, ctx, &ev.fields, true)?;
1717    write_dedup_string(w, ctx, ev.environment_variable_name.as_deref())?;
1718    if ctx.file_format_version >= 22 {
1719        write_7bit_int(w, ev.line)?;
1720        write_7bit_int(w, ev.column)?;
1721        write_dedup_string(w, ctx, ev.file_name.as_deref())?;
1722    }
1723    Ok(())
1724}
1725
1726/// Write a `ResponseFileUsed` event.
1727pub fn write_response_file_used<W: Write>(
1728    w: &mut W,
1729    ctx: &mut WriteContext,
1730    ev: &ResponseFileUsedEvent,
1731) -> std::io::Result<()> {
1732    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1733    write_dedup_string(w, ctx, ev.response_file_path.as_deref())
1734}
1735
1736/// Write an `AssemblyLoad` event.
1737pub fn write_assembly_load<W: Write>(
1738    w: &mut W,
1739    ctx: &mut WriteContext,
1740    ev: &AssemblyLoadEvent,
1741) -> std::io::Result<()> {
1742    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1743    write_7bit_int(w, ev.context)?;
1744    write_dedup_string(w, ctx, ev.loading_initiator.as_deref())?;
1745    write_dedup_string(w, ctx, ev.assembly_name.as_deref())?;
1746    write_dedup_string(w, ctx, ev.assembly_path.as_deref())?;
1747    write_guid(w, &ev.mvid)?;
1748    write_dedup_string(w, ctx, ev.app_domain_name.as_deref())
1749}
1750
1751/// Write a `ProjectImported` event.
1752pub fn write_project_imported<W: Write>(
1753    w: &mut W,
1754    ctx: &mut WriteContext,
1755    ev: &ProjectImportedEvent,
1756) -> std::io::Result<()> {
1757    write_build_event_args_fields(w, ctx, &ev.fields, true)?;
1758    if ctx.file_format_version > 2 {
1759        write_bool(w, ev.import_ignored)?;
1760    }
1761    write_optional_string(w, ctx, ev.imported_project_file.as_deref())?;
1762    write_optional_string(w, ctx, ev.unexpanded_project.as_deref())
1763}
1764
1765/// Write a `BuildCheckTracing` event.
1766pub fn write_build_check_tracing<W: Write>(
1767    w: &mut W,
1768    ctx: &mut WriteContext,
1769    ev: &BuildCheckTracingEvent,
1770) -> std::io::Result<()> {
1771    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1772    write_string_dictionary(w, ctx, ev.tracing_data.as_deref())
1773}
1774
1775/// Write a `BuildCheckAcquisition` event.
1776pub fn write_build_check_acquisition<W: Write>(
1777    w: &mut W,
1778    ctx: &mut WriteContext,
1779    ev: &BuildCheckAcquisitionEvent,
1780) -> std::io::Result<()> {
1781    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1782    write_dotnet_string(w, &ev.acquisition_path)?;
1783    write_dotnet_string(w, &ev.project_path)
1784}
1785
1786/// Write a `BuildSubmissionStarted` event.
1787pub fn write_build_submission_started<W: Write>(
1788    w: &mut W,
1789    ctx: &mut WriteContext,
1790    ev: &BuildSubmissionStartedEvent,
1791) -> std::io::Result<()> {
1792    write_build_event_args_fields(w, ctx, &ev.fields, false)?;
1793    write_string_dictionary(w, ctx, ev.global_properties.as_deref())?;
1794    write_string_list(w, ctx, ev.entry_projects_full_path.as_deref())?;
1795    write_string_list(w, ctx, ev.target_names.as_deref())?;
1796    write_7bit_int(w, ev.flags)?;
1797    write_7bit_int(w, ev.submission_id)
1798}
1799
1800/// Write a `BuildCanceled` event.
1801pub fn write_build_canceled<W: Write>(
1802    w: &mut W,
1803    ctx: &mut WriteContext,
1804    ev: &BuildCanceledEvent,
1805) -> std::io::Result<()> {
1806    write_build_event_args_fields(w, ctx, &ev.fields, false)
1807}
1808
1809#[cfg(test)]
1810mod tests;
1811
1812#[cfg(test)]
1813mod write_tests;