macos_unifiedlogs/
unified_log.rs

1// Copyright 2022 Mandiant, Inc. All Rights Reserved
2// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
3// http://www.apache.org/licenses/LICENSE-2.0
4// Unless required by applicable law or agreed to in writing, software distributed under the License
5// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6// See the License for the specific language governing permissions and limitations under the License.
7
8//! Parse macOS Unified Log data
9//!
10//! Provides a simple library to parse the macOS Unified Log format.
11
12use std::collections::HashMap;
13
14use crate::catalog::CatalogChunk;
15use crate::chunks::firehose::activity::FirehoseActivity;
16use crate::chunks::firehose::firehose_log::{Firehose, FirehoseItemInfo, FirehosePreamble};
17use crate::chunks::firehose::nonactivity::FirehoseNonActivity;
18use crate::chunks::firehose::signpost::FirehoseSignpost;
19use crate::chunks::firehose::trace::FirehoseTrace;
20use crate::chunks::oversize::Oversize;
21use crate::chunks::simpledump::SimpleDump;
22use crate::chunks::statedump::Statedump;
23use crate::chunkset::ChunksetChunk;
24use crate::header::HeaderChunk;
25use crate::message::format_firehose_log_message;
26use crate::preamble::LogPreamble;
27use crate::timesync::TimesyncBoot;
28use crate::traits::FileProvider;
29use crate::util::{
30    encode_standard, extract_string, padding_size_8, u64_to_usize, unixepoch_to_iso,
31};
32use log::{error, warn};
33use nom::bytes::complete::take;
34use regex::Regex;
35use serde::Serialize;
36use sunlight::light::extract_protobuf;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
39pub enum LogType {
40    Debug,
41    Info,
42    Default,
43    Error,
44    Fault,
45    Create,
46    Useraction,
47    ProcessSignpostEvent,
48    ProcessSignpostStart,
49    ProcessSignpostEnd,
50    SystemSignpostEvent,
51    SystemSignpostStart,
52    SystemSignpostEnd,
53    ThreadSignpostEvent,
54    ThreadSignpostStart,
55    ThreadSignpostEnd,
56    Simpledump,
57    Statedump,
58    Loss,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
62pub enum EventType {
63    Unknown,
64    Log,
65    Activity,
66    Trace,
67    Signpost,
68    Simpledump,
69    Statedump,
70    Loss,
71}
72
73#[derive(Debug, Clone, Default)]
74pub struct UnifiedLogData {
75    pub header: Vec<HeaderChunk>,
76    pub catalog_data: Vec<UnifiedLogCatalogData>,
77    /// Keep a global cache of oversize string
78    pub oversize: Vec<Oversize>,
79}
80
81#[derive(Debug, Clone, Default)]
82pub struct UnifiedLogCatalogData {
83    pub catalog: CatalogChunk,
84    pub firehose: Vec<FirehosePreamble>,
85    pub simpledump: Vec<SimpleDump>,
86    pub statedump: Vec<Statedump>,
87    pub oversize: Vec<Oversize>,
88}
89
90struct LogIterator<'a> {
91    unified_log_data: &'a UnifiedLogData,
92    provider: &'a mut dyn FileProvider,
93    timesync_data: &'a HashMap<String, TimesyncBoot>,
94    exclude_missing: bool,
95    message_re: Regex,
96    catalog_data_iterator_index: usize,
97}
98impl<'a> LogIterator<'a> {
99    fn new(
100        unified_log_data: &'a UnifiedLogData,
101        provider: &'a mut dyn FileProvider,
102        timesync_data: &'a HashMap<String, TimesyncBoot>,
103        exclude_missing: bool,
104    ) -> Result<Self, regex::Error> {
105        /*
106        Crazy Regex to try to get all log message formatters
107        Formatters are based off of printf formatters with additional Apple values
108        (                                 # start of capture group 1
109        %                                 # literal "%"
110        (?:                               # first option
111
112        (?:{[^}]+}?)                      # Get String formatters with %{<variable>}<variable> values. Ex: %{public}#llx with team ID %{public}@
113        (?:[-+0#]{0,5})                   # optional flags
114        (?:\d+|\*)?                       # width
115        (?:\.(?:\d+|\*))?                 # precision
116        (?:h|hh|l|ll|t|q|w|I|z|I32|I64)?  # size
117        [cCdiouxXeEfgGaAnpsSZPm@}]       # type
118
119        |                                 # OR get regular string formatters, ex: %s, %d
120
121        (?:[-+0 #]{0,5})                  # optional flags
122        (?:\d+|\*)?                       # width
123        (?:\.(?:\d+|\*))?                 # precision
124        (?:h|hh|l|ll|w|I|t|q|z|I32|I64)?  # size
125        [cCdiouxXeEfgGaAnpsSZPm@%]        # type
126        ))
127        */
128        let message_re_result = Regex::new(
129            r"(%(?:(?:\{[^}]+}?)(?:[-+0#]{0,5})(?:\d+|\*)?(?:\.(?:\d+|\*))?(?:h|hh|l|ll|w|I|z|t|q|I32|I64)?[cmCdiouxXeEfgGaAnpsSZP@}]|(?:[-+0 #]{0,5})(?:\d+|\*)?(?:\.(?:\d+|\*))?(?:h|hh|l||q|t|ll|w|I|z|I32|I64)?[cmCdiouxXeEfgGaAnpsSZP@%]))",
130        );
131        let message_re = match message_re_result {
132            Ok(message_re) => message_re,
133            Err(err) => {
134                error!("Failed to compile regex for printf format parsing: {err:?}");
135                return Err(err);
136            }
137        };
138
139        Ok(LogIterator {
140            unified_log_data,
141            provider,
142            timesync_data,
143            exclude_missing,
144            message_re,
145            catalog_data_iterator_index: 0,
146        })
147    }
148}
149
150impl Iterator for LogIterator<'_> {
151    type Item = (Vec<LogData>, UnifiedLogData);
152
153    /// `catalog_data_index` == 0
154    fn next(&mut self) -> Option<Self::Item> {
155        let catalog_data = self
156            .unified_log_data
157            .catalog_data
158            .get(self.catalog_data_iterator_index)?;
159        let mut log_data_vec: Vec<LogData> = Vec::new();
160        // Need to keep track of any log entries that fail to find Oversize strings (sometimes the strings may be in other log files that have not been parsed yet)
161        let mut missing_unified_log_data_vec = UnifiedLogData {
162            header: Vec::new(),
163            catalog_data: Vec::new(),
164            oversize: Vec::new(),
165        };
166
167        for (preamble_index, preamble) in catalog_data.firehose.iter().enumerate() {
168            for (firehose_index, firehose) in preamble.public_data.iter().enumerate() {
169                // The continous time is actually 6 bytes long. Combining 4 bytes and 2 bytes
170                let firehose_log_entry_continous_time = u64::from(firehose.continous_time_delta)
171                    | ((u64::from(firehose.continous_time_delta_upper)) << 32);
172
173                let continous_time =
174                    preamble.base_continous_time + firehose_log_entry_continous_time;
175
176                // Calculate the timestamp for the log entry
177                let timestamp = TimesyncBoot::get_timestamp(
178                    self.timesync_data,
179                    &self.unified_log_data.header[0].boot_uuid,
180                    continous_time,
181                    preamble.base_continous_time,
182                );
183
184                // Our struct format to hold and show the log data
185                let mut log_data = LogData {
186                    subsystem: String::new(),
187                    thread_id: firehose.thread_id,
188                    pid: catalog_data.catalog.get_pid(
189                        preamble.first_number_proc_id,
190                        preamble.second_number_proc_id,
191                    ),
192                    library: String::new(),
193                    activity_id: 0,
194                    time: timestamp,
195                    timestamp: unixepoch_to_iso(&(timestamp as i64)),
196                    category: String::new(),
197                    log_type: LogData::get_log_type(
198                        firehose.unknown_log_type,
199                        firehose.unknown_log_activity_type,
200                    ),
201                    process: String::new(),
202                    message: String::new(),
203                    event_type: LogData::get_event_type(firehose.unknown_log_activity_type),
204                    euid: catalog_data.catalog.get_euid(
205                        preamble.first_number_proc_id,
206                        preamble.second_number_proc_id,
207                    ),
208                    boot_uuid: self.unified_log_data.header[0].boot_uuid.to_owned(),
209                    timezone_name: self.unified_log_data.header[0]
210                        .timezone_path
211                        .split('/')
212                        .next_back()
213                        .unwrap_or("Unknown Timezone Name")
214                        .to_string(),
215                    library_uuid: String::new(),
216                    process_uuid: String::new(),
217                    raw_message: String::new(),
218                    message_entries: firehose.message.item_info.to_owned(),
219                };
220
221                // 0x4 - Non-activity log entry. Ex: log default, log error, etc
222                // 0x2 - Activity log entry. Ex: activity create
223                // 0x7 - Loss log entry. Ex: loss
224                // 0x6 - Signpost entry. Ex: process signpost, thread signpost, system signpost
225                // 0x3 - Trace log entry. Ex: trace default
226                match firehose.unknown_log_activity_type {
227                    0x4 => {
228                        log_data.activity_id =
229                            u64::from(firehose.firehose_non_activity.unknown_activity_id);
230                        let message_data = FirehoseNonActivity::get_firehose_nonactivity_strings(
231                            &firehose.firehose_non_activity,
232                            self.provider,
233                            u64::from(firehose.format_string_location),
234                            &preamble.first_number_proc_id,
235                            &preamble.second_number_proc_id,
236                            &catalog_data.catalog,
237                        );
238
239                        match message_data {
240                            Ok((_, results)) => {
241                                log_data.library = results.library;
242                                log_data.library_uuid = results.library_uuid;
243                                log_data.process = results.process;
244                                log_data.process_uuid = results.process_uuid;
245                                results.format_string.clone_into(&mut log_data.raw_message);
246
247                                // If the non-activity log entry has a data ref value then the message strings are stored in an oversize log entry
248                                let log_message =
249                                    if firehose.firehose_non_activity.data_ref_value != 0 {
250                                        let oversize_strings = Oversize::get_oversize_strings(
251                                            firehose.firehose_non_activity.data_ref_value,
252                                            preamble.first_number_proc_id,
253                                            preamble.second_number_proc_id,
254                                            &self.unified_log_data.oversize,
255                                        );
256                                        // Format and map the log strings with the message format string found UUIDText or shared string file
257                                        format_firehose_log_message(
258                                            results.format_string,
259                                            &oversize_strings,
260                                            &self.message_re,
261                                        )
262                                    } else {
263                                        // Format and map the log strings with the message format string found UUIDText or shared string file
264                                        format_firehose_log_message(
265                                            results.format_string,
266                                            &firehose.message.item_info,
267                                            &self.message_re,
268                                        )
269                                    };
270                                // If we are tracking missing data (due to it being stored in another log file). Add missing data to vec to track and parse again once we got all data
271                                if self.exclude_missing
272                                    && log_message.contains("<Missing message data>")
273                                {
274                                    LogData::add_missing(
275                                        catalog_data,
276                                        preamble_index,
277                                        firehose_index,
278                                        &self.unified_log_data.header,
279                                        &mut missing_unified_log_data_vec,
280                                        preamble,
281                                    );
282                                    continue;
283                                }
284
285                                if !firehose.message.backtrace_strings.is_empty() {
286                                    log_data.message = format!(
287                                        "Backtrace:\n{}\n{}",
288                                        firehose.message.backtrace_strings.join("\n"),
289                                        log_message
290                                    );
291                                } else {
292                                    log_data.message = log_message;
293                                }
294                            }
295                            Err(err) => {
296                                warn!(
297                                    "[macos-unifiedlogs] Failed to get message string data for firehose non-activity log entry: {err:?}"
298                                );
299                            }
300                        }
301
302                        if firehose.firehose_non_activity.subsystem_value != 0 {
303                            let results = catalog_data.catalog.get_subsystem(
304                                firehose.firehose_non_activity.subsystem_value,
305                                preamble.first_number_proc_id,
306                                preamble.second_number_proc_id,
307                            );
308                            match results {
309                                Ok((_, subsystem)) => {
310                                    log_data.subsystem = subsystem.subsystem;
311                                    log_data.category = subsystem.category;
312                                }
313                                Err(err) => {
314                                    warn!("[macos-unifiedlogs] Failed to get subsystem: {err:?}")
315                                }
316                            }
317                        }
318                    }
319                    0x7 => {
320                        // No message data in loss entries
321                        log_data.event_type = EventType::Loss;
322                        log_data.log_type = LogType::Loss;
323                    }
324                    0x2 => {
325                        log_data.activity_id =
326                            u64::from(firehose.firehose_activity.unknown_activity_id);
327                        let message_data = FirehoseActivity::get_firehose_activity_strings(
328                            &firehose.firehose_activity,
329                            self.provider,
330                            u64::from(firehose.format_string_location),
331                            &preamble.first_number_proc_id,
332                            &preamble.second_number_proc_id,
333                            &catalog_data.catalog,
334                        );
335                        match message_data {
336                            Ok((_, results)) => {
337                                log_data.library = results.library;
338                                log_data.library_uuid = results.library_uuid;
339                                log_data.process = results.process;
340                                log_data.process_uuid = results.process_uuid;
341                                results.format_string.clone_into(&mut log_data.raw_message);
342
343                                let log_message = format_firehose_log_message(
344                                    results.format_string,
345                                    &firehose.message.item_info,
346                                    &self.message_re,
347                                );
348
349                                if self.exclude_missing
350                                    && log_message.contains("<Missing message data>")
351                                {
352                                    LogData::add_missing(
353                                        catalog_data,
354                                        preamble_index,
355                                        firehose_index,
356                                        &self.unified_log_data.header,
357                                        &mut missing_unified_log_data_vec,
358                                        preamble,
359                                    );
360                                    continue;
361                                }
362                                if !firehose.message.backtrace_strings.is_empty() {
363                                    log_data.message = format!(
364                                        "Backtrace:\n{}\n{}",
365                                        firehose.message.backtrace_strings.join("\n"),
366                                        log_message
367                                    );
368                                } else {
369                                    log_data.message = log_message;
370                                }
371                            }
372                            Err(err) => {
373                                warn!(
374                                    "[macos-unifiedlogs] Failed to get message string data for firehose activity log entry: {err:?}"
375                                );
376                            }
377                        }
378                    }
379                    0x6 => {
380                        log_data.activity_id =
381                            u64::from(firehose.firehose_signpost.unknown_activity_id);
382                        let message_data = FirehoseSignpost::get_firehose_signpost(
383                            &firehose.firehose_signpost,
384                            self.provider,
385                            u64::from(firehose.format_string_location),
386                            &preamble.first_number_proc_id,
387                            &preamble.second_number_proc_id,
388                            &catalog_data.catalog,
389                        );
390                        match message_data {
391                            Ok((_, results)) => {
392                                log_data.library = results.library;
393                                log_data.library_uuid = results.library_uuid;
394                                log_data.process = results.process;
395                                log_data.process_uuid = results.process_uuid;
396                                results.format_string.clone_into(&mut log_data.raw_message);
397
398                                let mut log_message =
399                                    if firehose.firehose_non_activity.data_ref_value != 0 {
400                                        let oversize_strings = Oversize::get_oversize_strings(
401                                            firehose.firehose_non_activity.data_ref_value,
402                                            preamble.first_number_proc_id,
403                                            preamble.second_number_proc_id,
404                                            &self.unified_log_data.oversize,
405                                        );
406                                        // Format and map the log strings with the message format string found UUIDText or shared string file
407                                        format_firehose_log_message(
408                                            results.format_string,
409                                            &oversize_strings,
410                                            &self.message_re,
411                                        )
412                                    } else {
413                                        // Format and map the log strings with the message format string found UUIDText or shared string file
414                                        format_firehose_log_message(
415                                            results.format_string,
416                                            &firehose.message.item_info,
417                                            &self.message_re,
418                                        )
419                                    };
420                                if self.exclude_missing
421                                    && log_message.contains("<Missing message data>")
422                                {
423                                    LogData::add_missing(
424                                        catalog_data,
425                                        preamble_index,
426                                        firehose_index,
427                                        &self.unified_log_data.header,
428                                        &mut missing_unified_log_data_vec,
429                                        preamble,
430                                    );
431                                    continue;
432                                }
433
434                                log_message = format!(
435                                    "Signpost ID: {:X} - Signpost Name: {:X}\n {log_message}",
436                                    firehose.firehose_signpost.signpost_id,
437                                    firehose.firehose_signpost.signpost_name,
438                                );
439
440                                if !firehose.message.backtrace_strings.is_empty() {
441                                    log_data.message = format!(
442                                        "Backtrace:\n{}\n{log_message}",
443                                        firehose.message.backtrace_strings.join("\n"),
444                                    );
445                                } else {
446                                    log_data.message = log_message;
447                                }
448                            }
449                            Err(err) => {
450                                warn!(
451                                    "[macos-unifiedlogs] Failed to get message string data for firehose signpost log entry: {err:?}"
452                                );
453                            }
454                        }
455                        if firehose.firehose_signpost.subsystem != 0 {
456                            let results = catalog_data.catalog.get_subsystem(
457                                firehose.firehose_signpost.subsystem,
458                                preamble.first_number_proc_id,
459                                preamble.second_number_proc_id,
460                            );
461                            match results {
462                                Ok((_, subsystem)) => {
463                                    log_data.subsystem = subsystem.subsystem;
464                                    log_data.category = subsystem.category;
465                                }
466                                Err(err) => {
467                                    warn!("[macos-unifiedlogs] Failed to get subsystem: {err:?}")
468                                }
469                            }
470                        }
471                    }
472                    0x3 => {
473                        let message_data = FirehoseTrace::get_firehose_trace_strings(
474                            self.provider,
475                            u64::from(firehose.format_string_location),
476                            &preamble.first_number_proc_id,
477                            &preamble.second_number_proc_id,
478                            &catalog_data.catalog,
479                        );
480                        match message_data {
481                            Ok((_, results)) => {
482                                log_data.library = results.library;
483                                log_data.library_uuid = results.library_uuid;
484                                log_data.process = results.process;
485                                log_data.process_uuid = results.process_uuid;
486
487                                let log_message = format_firehose_log_message(
488                                    results.format_string,
489                                    &firehose.message.item_info,
490                                    &self.message_re,
491                                );
492
493                                if self.exclude_missing
494                                    && log_message.contains("<Missing message data>")
495                                {
496                                    LogData::add_missing(
497                                        catalog_data,
498                                        preamble_index,
499                                        firehose_index,
500                                        &self.unified_log_data.header,
501                                        &mut missing_unified_log_data_vec,
502                                        preamble,
503                                    );
504                                    continue;
505                                }
506                                if !firehose.message.backtrace_strings.is_empty() {
507                                    log_data.message = format!(
508                                        "Backtrace:\n{}\n{log_message}",
509                                        firehose.message.backtrace_strings.join("\n"),
510                                    );
511                                } else {
512                                    log_data.message = log_message;
513                                }
514                            }
515                            Err(err) => {
516                                warn!(
517                                    "[macos-unifiedlogs] Failed to get message string data for firehose activity log entry: {err:?}",
518                                );
519                            }
520                        }
521                    }
522                    _ => {
523                        error!("[macos-unifiedlogs] Parsed unknown log firehose data: {firehose:?}",)
524                    }
525                }
526                log_data_vec.push(log_data);
527            }
528        }
529
530        for simpledump in &catalog_data.simpledump {
531            let no_firehose_preamble = 1;
532            let timestamp = TimesyncBoot::get_timestamp(
533                self.timesync_data,
534                &self.unified_log_data.header[0].boot_uuid,
535                simpledump.continous_time,
536                no_firehose_preamble,
537            );
538            let log_data = LogData {
539                subsystem: simpledump.subsystem.to_owned(),
540                thread_id: simpledump.thread_id,
541                pid: simpledump.first_proc_id,
542                library: String::new(),
543                activity_id: 0,
544                time: timestamp,
545                timestamp: unixepoch_to_iso(&(timestamp as i64)),
546                category: String::new(),
547                log_type: LogType::Simpledump,
548                process: String::new(),
549                message: simpledump.message_string.to_owned(),
550                event_type: EventType::Simpledump,
551                euid: 0,
552                boot_uuid: self.unified_log_data.header[0].boot_uuid.to_owned(),
553                timezone_name: self.unified_log_data.header[0]
554                    .timezone_path
555                    .split('/')
556                    .next_back()
557                    .unwrap_or("Unknown Timezone Name")
558                    .to_string(),
559                library_uuid: simpledump.sender_uuid.to_owned(),
560                process_uuid: simpledump.dsc_uuid.to_owned(),
561                raw_message: String::new(),
562                message_entries: Vec::new(),
563            };
564            log_data_vec.push(log_data);
565        }
566
567        for statedump in &catalog_data.statedump {
568            let no_firehose_preamble = 1;
569
570            let data_string = match statedump.unknown_data_type {
571                0x1 => Statedump::parse_statedump_plist(&statedump.statedump_data),
572                0x2 => match extract_protobuf(&statedump.statedump_data) {
573                    Ok(map) => serde_json::to_string(&map)
574                        .unwrap_or(String::from("Failed to serialize Protobuf HashMap")),
575                    Err(_err) => format!(
576                        "Failed to parse StateDump protobuf: {}",
577                        encode_standard(&statedump.statedump_data)
578                    ),
579                },
580                0x3 => Statedump::parse_statedump_object(
581                    &statedump.statedump_data,
582                    &statedump.title_name,
583                ),
584                _ => {
585                    warn!(
586                        "Unknown statedump data type: {}",
587                        statedump.unknown_data_type
588                    );
589                    let results = extract_string(&statedump.statedump_data);
590                    match results {
591                        Ok((_, string_data)) => string_data,
592                        Err(err) => {
593                            error!(
594                                "[macos-unifiedlogs] Failed to extract string from statedump: {err:?}"
595                            );
596                            String::from("Failed to extract string from statedump")
597                        }
598                    }
599                }
600            };
601            let timestamp = TimesyncBoot::get_timestamp(
602                self.timesync_data,
603                &self.unified_log_data.header[0].boot_uuid,
604                statedump.continuous_time,
605                no_firehose_preamble,
606            );
607            let log_data = LogData {
608                subsystem: String::new(),
609                thread_id: 0,
610                pid: statedump.first_proc_id,
611                library: String::new(),
612                activity_id: statedump.activity_id,
613                time: timestamp,
614                timestamp: unixepoch_to_iso(&(timestamp as i64)),
615                category: String::new(),
616                event_type: EventType::Statedump,
617                process: String::new(),
618                message: format!(
619                    "title: {}\nObject Type: {}\nObject Type: {}\n{data_string}",
620                    statedump.title_name, statedump.decoder_library, statedump.decoder_type,
621                ),
622                log_type: LogType::Statedump,
623                euid: 0,
624                boot_uuid: self.unified_log_data.header[0].boot_uuid.to_owned(),
625                timezone_name: self.unified_log_data.header[0]
626                    .timezone_path
627                    .split('/')
628                    .next_back()
629                    .unwrap_or("Unknown Timezone Name")
630                    .to_string(),
631                library_uuid: String::new(),
632                process_uuid: String::new(),
633                raw_message: String::new(),
634                message_entries: Vec::new(),
635            };
636            log_data_vec.push(log_data);
637        }
638
639        self.catalog_data_iterator_index += 1;
640        Some((log_data_vec, missing_unified_log_data_vec))
641    }
642}
643
644#[derive(Debug, Serialize)]
645pub struct LogData {
646    pub subsystem: String,
647    pub thread_id: u64,
648    pub pid: u64,
649    pub euid: u32,
650    pub library: String,
651    pub library_uuid: String,
652    pub activity_id: u64,
653    pub time: f64,
654    pub category: String,
655    pub event_type: EventType,
656    pub log_type: LogType,
657    pub process: String,
658    pub process_uuid: String,
659    pub message: String,
660    pub raw_message: String,
661    pub boot_uuid: String,
662    pub timezone_name: String,
663    pub message_entries: Vec<FirehoseItemInfo>,
664    pub timestamp: String,
665}
666
667impl LogData {
668    /// Parse the Unified log data read from a tracev3 file
669    pub fn parse_unified_log(data: &[u8]) -> nom::IResult<&[u8], UnifiedLogData> {
670        let mut unified_log_data_true = UnifiedLogData {
671            header: Vec::new(),
672            catalog_data: Vec::new(),
673            oversize: Vec::new(),
674        };
675
676        let mut catalog_data = UnifiedLogCatalogData::default();
677
678        let mut input = data;
679        let chunk_preamble_size = 16; // Include preamble size in total chunk size
680
681        let header_chunk = 0x1000;
682        let catalog_chunk = 0x600b;
683        let chunkset_chunk = 0x600d;
684        // Loop through traceV3 file until all file contents are read
685        while !input.is_empty() {
686            let (_, preamble) = LogPreamble::detect_preamble(input)?;
687            let chunk_size = preamble.chunk_data_size;
688
689            // Grab all data associated with Unified Log entry (chunk)
690            let chunk_size = match u64_to_usize(chunk_size) {
691                Some(c) => c,
692                None => {
693                    error!("[macos-unifiedlogs] u64 is bigger than system usize");
694                    return Err(nom::Err::Error(nom::error::Error::new(
695                        data,
696                        nom::error::ErrorKind::TooLarge,
697                    )));
698                }
699            };
700            let (data, chunk_data) = take(chunk_size + chunk_preamble_size)(input)?;
701
702            if preamble.chunk_tag == header_chunk {
703                LogData::get_header_data(chunk_data, &mut unified_log_data_true);
704            } else if preamble.chunk_tag == catalog_chunk {
705                if catalog_data.catalog.chunk_tag != 0 {
706                    unified_log_data_true.catalog_data.push(catalog_data);
707                }
708                catalog_data = UnifiedLogCatalogData::default();
709
710                LogData::get_catalog_data(chunk_data, &mut catalog_data);
711            } else if preamble.chunk_tag == chunkset_chunk {
712                LogData::get_chunkset_data(
713                    chunk_data,
714                    &mut catalog_data,
715                    &mut unified_log_data_true,
716                );
717            } else {
718                error!(
719                    "[macos-unifiedlogs] Unknown chunk type: {:?}",
720                    preamble.chunk_tag
721                );
722            }
723
724            let padding_size = padding_size_8(preamble.chunk_data_size);
725            if data.len() < padding_size as usize {
726                break;
727            }
728            let padding_size = match u64_to_usize(padding_size) {
729                Some(p) => p,
730                None => {
731                    error!("[macos-unifiedlogs] u64 is bigger than system usize");
732                    return Err(nom::Err::Error(nom::error::Error::new(
733                        data,
734                        nom::error::ErrorKind::TooLarge,
735                    )));
736                }
737            };
738
739            let (data, _) = take(padding_size)(data)?;
740            if data.is_empty() {
741                break;
742            }
743            input = data;
744            if input.len() < chunk_preamble_size {
745                warn!(
746                    "Not enough data for preamble header, needed 16 bytes. Got: {:?}",
747                    input.len()
748                );
749                break;
750            }
751        }
752        // Make sure to get the last catalog
753        if catalog_data.catalog.chunk_tag != 0 {
754            unified_log_data_true.catalog_data.push(catalog_data);
755        }
756        Ok((input, unified_log_data_true))
757    }
758
759    /// Reconstruct Unified Log entries using the binary strings data, cached strings data, timesync data, and unified log. Provide bool to ignore log entries that are not able to be recontructed (additional tracev3 files needed)
760    /// Return a reconstructed log entries and any leftover Unified Log entries that could not be reconstructed (data may be stored in other tracev3 files)
761    pub fn build_log(
762        unified_log_data: &UnifiedLogData,
763        provider: &mut dyn FileProvider,
764        timesync_data: &HashMap<String, TimesyncBoot>,
765        exclude_missing: bool,
766    ) -> (Vec<LogData>, UnifiedLogData) {
767        let mut log_data_vec: Vec<LogData> = Vec::new();
768        // Need to keep track of any log entries that fail to find Oversize strings (sometimes the strings may be in other log files that have not been parsed yet)
769        let mut missing_unified_log_data_vec = UnifiedLogData {
770            header: Vec::new(),
771            catalog_data: Vec::new(),
772            oversize: Vec::new(),
773        };
774
775        let Ok(log_iterator) =
776            LogIterator::new(unified_log_data, provider, timesync_data, exclude_missing)
777        else {
778            return (log_data_vec, missing_unified_log_data_vec);
779        };
780        for (mut log_data, mut missing_unified_log) in log_iterator {
781            log_data_vec.append(&mut log_data);
782            missing_unified_log_data_vec
783                .header
784                .append(&mut missing_unified_log.header);
785            missing_unified_log_data_vec
786                .catalog_data
787                .append(&mut missing_unified_log.catalog_data);
788            missing_unified_log_data_vec
789                .oversize
790                .append(&mut missing_unified_log.oversize);
791        }
792
793        (log_data_vec, missing_unified_log_data_vec)
794    }
795
796    /// Return log type based on parsed log data
797    fn get_log_type(log_type: u8, activity_type: u8) -> LogType {
798        match log_type {
799            0x1 => {
800                let activity = 2;
801                if activity_type == activity {
802                    LogType::Create
803                } else {
804                    LogType::Info
805                }
806            }
807            0x2 => LogType::Debug,
808            0x3 => LogType::Useraction,
809            0x10 => LogType::Error,
810            0x11 => LogType::Fault,
811            0x80 => LogType::ProcessSignpostEvent,
812            0x81 => LogType::ProcessSignpostStart,
813            0x82 => LogType::ProcessSignpostEnd,
814            0xc0 => LogType::SystemSignpostEvent, // Not seen but may exist?
815            0xc1 => LogType::SystemSignpostStart,
816            0xc2 => LogType::SystemSignpostEnd,
817            0x40 => LogType::ThreadSignpostEvent, // Not seen but may exist?
818            0x41 => LogType::ThreadSignpostStart,
819            0x42 => LogType::ThreadSignpostEnd,
820            _ => LogType::Default,
821        }
822    }
823
824    /// Return the log event type based on parsed log data
825    fn get_event_type(event_type: u8) -> EventType {
826        match event_type {
827            0x4 => EventType::Log,
828            0x2 => EventType::Activity,
829            0x3 => EventType::Trace,
830            0x6 => EventType::Signpost,
831            0x7 => EventType::Loss,
832            _ => EventType::Unknown,
833        }
834    }
835
836    /// Get the header of the Unified Log data (tracev3 file)
837    pub(crate) fn get_header_data(data: &[u8], unified_log_data: &mut UnifiedLogData) {
838        let header_results = HeaderChunk::parse_header(data);
839        match header_results {
840            Ok((_, header_data)) => unified_log_data.header.push(header_data),
841            Err(err) => error!("[macos-unifiedlogs] Failed to parse header data: {err:?}"),
842        }
843    }
844
845    /// Get the Catalog of the Unified Log data (tracev3 file)
846    pub(crate) fn get_catalog_data(data: &[u8], unified_log_data: &mut UnifiedLogCatalogData) {
847        let catalog_results = CatalogChunk::parse_catalog(data);
848        match catalog_results {
849            Ok((_, catalog_data)) => unified_log_data.catalog = catalog_data,
850            Err(err) => error!("[macos-unifiedlogs] Failed to parse catalog data: {err:?}"),
851        }
852    }
853
854    /// Get the Chunkset of the Unified Log data (tracev3)
855    pub(crate) fn get_chunkset_data(
856        data: &[u8],
857        catalog_data: &mut UnifiedLogCatalogData,
858        unified_log_data: &mut UnifiedLogData,
859    ) {
860        // Parse and decompress the chunkset entries
861        let chunkset_data_results = ChunksetChunk::parse_chunkset(data);
862        match chunkset_data_results {
863            Ok((_, chunkset_data)) => {
864                // Parse the decompressed data which contains the log data
865                let _result = ChunksetChunk::parse_chunkset_data(
866                    &chunkset_data.decompressed_data,
867                    catalog_data,
868                );
869                unified_log_data.oversize.append(&mut catalog_data.oversize);
870            }
871            Err(err) => error!("[macos-unifiedlogs] Failed to parse chunkset data: {err:?}"),
872        }
873    }
874
875    /// Track log entries that are missing data that could in another tracev3 file
876    fn track_missing(
877        first_proc_id: u64,
878        second_proc_id: u32,
879        time: u64,
880        firehose: Firehose,
881    ) -> FirehosePreamble {
882        FirehosePreamble {
883            chunk_tag: 0,
884            chunk_sub_tag: 0,
885            chunk_data_size: 0,
886            first_number_proc_id: first_proc_id,
887            second_number_proc_id: second_proc_id,
888            collapsed: 0,
889            unknown: Vec::new(),
890            public_data_size: 0,
891            private_data_virtual_offset: 0,
892            unkonwn2: 0,
893            unknown3: 0,
894            base_continous_time: time,
895            public_data: vec![firehose],
896            ttl: 0,
897        }
898    }
899
900    /// Add all missing log entries to log data tracker. Log data may be in another file. Mainly related to logs with that have Oversize data
901    fn add_missing(
902        catalog_data: &UnifiedLogCatalogData,
903        preamble_index: usize,
904        firehose_index: usize,
905        header: &[HeaderChunk],
906        missing_unified_log_data_vec: &mut UnifiedLogData,
907        preamble: &FirehosePreamble,
908    ) {
909        let missing_firehose = LogData::track_missing(
910            catalog_data.firehose[preamble_index].first_number_proc_id,
911            catalog_data.firehose[preamble_index].second_number_proc_id,
912            catalog_data.firehose[preamble_index].base_continous_time,
913            preamble.public_data[firehose_index].to_owned(),
914        );
915        let mut missing_unified_log_data = UnifiedLogCatalogData {
916            catalog: catalog_data.catalog.to_owned(),
917            firehose: Vec::new(),
918            simpledump: Vec::new(),
919            statedump: Vec::new(),
920            oversize: Vec::new(),
921        };
922
923        missing_unified_log_data.firehose.push(missing_firehose);
924        header.clone_into(&mut missing_unified_log_data_vec.header);
925
926        missing_unified_log_data_vec
927            .catalog_data
928            .push(missing_unified_log_data);
929    }
930}
931
932#[cfg(test)]
933mod tests {
934    use super::{LogData, UnifiedLogData};
935    use crate::{
936        chunks::firehose::firehose_log::Firehose,
937        filesystem::LogarchiveProvider,
938        parser::{collect_timesync, parse_log},
939        unified_log::{EventType, LogType, UnifiedLogCatalogData},
940    };
941    use std::{fs, path::PathBuf};
942
943    #[test]
944    fn test_parse_unified_log() {
945        let mut test_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
946        test_path.push(
947            "tests/test_data/system_logs_big_sur.logarchive/Persist/0000000000000002.tracev3",
948        );
949
950        let buffer = fs::read(test_path).unwrap();
951
952        let (_, results) = LogData::parse_unified_log(&buffer).unwrap();
953        assert_eq!(results.catalog_data.len(), 56);
954        assert_eq!(results.header.len(), 1);
955        assert_eq!(results.oversize.len(), 12);
956    }
957
958    #[test]
959    fn test_bad_log_header() {
960        let mut test_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
961        test_path.push("tests/test_data/Bad Data/TraceV3/Bad_header_0000000000000005.tracev3");
962
963        let buffer = fs::read(test_path).unwrap();
964        let (_, results) = LogData::parse_unified_log(&buffer).unwrap();
965        assert_eq!(results.catalog_data.len(), 36);
966        assert_eq!(results.header.len(), 0);
967        assert_eq!(results.oversize.len(), 28);
968    }
969
970    #[test]
971    #[cfg(target_pointer_width = "64")]
972    #[should_panic(expected = "Eof")]
973    fn test_bad_log_content_64() {
974        test_bad_log_content();
975    }
976
977    #[test]
978    #[cfg(target_pointer_width = "32")]
979    #[should_panic(expected = "TooLarge")]
980    fn test_bad_log_content_32() {
981        test_bad_log_content();
982    }
983
984    fn test_bad_log_content() {
985        let mut test_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
986        test_path.push("tests/test_data/Bad Data/TraceV3/Bad_content_0000000000000005.tracev3");
987
988        let buffer = fs::read(test_path).unwrap();
989        let (_, _) = LogData::parse_unified_log(&buffer).unwrap();
990    }
991
992    #[test]
993    #[cfg(target_pointer_width = "64")]
994    #[should_panic(expected = "Eof")]
995    fn test_bad_log_file_64() {
996        test_bad_log_file();
997    }
998
999    #[test]
1000    #[cfg(target_pointer_width = "32")]
1001    #[should_panic(expected = "TooLarge")]
1002    fn test_bad_log_file_32() {
1003        test_bad_log_file();
1004    }
1005
1006    fn test_bad_log_file() {
1007        let mut test_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1008        test_path.push("tests/test_data/Bad Data/TraceV3/00.tracev3");
1009
1010        let buffer = fs::read(test_path).unwrap();
1011        let (_, _) = LogData::parse_unified_log(&buffer).unwrap();
1012    }
1013
1014    #[test]
1015    fn test_build_log() {
1016        let mut test_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1017        test_path.push("tests/test_data/system_logs_big_sur.logarchive");
1018
1019        let mut provider = LogarchiveProvider::new(test_path.as_path());
1020        let timesync_data = collect_timesync(&provider).unwrap();
1021
1022        test_path.push("Persist/0000000000000002.tracev3");
1023
1024        let reader = std::fs::File::open(test_path).unwrap();
1025        let log_data = parse_log(reader).unwrap();
1026
1027        let exclude_missing = false;
1028        let (results, _) =
1029            LogData::build_log(&log_data, &mut provider, &timesync_data, exclude_missing);
1030
1031        assert_eq!(results.len(), 207366);
1032        assert_eq!(results[0].process, "/usr/libexec/lightsoutmanagementd");
1033        assert_eq!(results[0].subsystem, "com.apple.lom");
1034        assert_eq!(results[0].time, 1_642_302_326_434_850_800.0);
1035        assert_eq!(results[0].activity_id, 0);
1036        assert_eq!(results[0].library, "/usr/libexec/lightsoutmanagementd");
1037        assert_eq!(results[0].library_uuid, "6C3ADF991F033C1C96C4ADFAA12D8CED");
1038        assert_eq!(results[0].process_uuid, "6C3ADF991F033C1C96C4ADFAA12D8CED");
1039        assert_eq!(results[0].message, "LOMD Start");
1040        assert_eq!(results[0].pid, 45);
1041        assert_eq!(results[0].thread_id, 588);
1042        assert_eq!(results[0].category, "device");
1043        assert_eq!(results[0].log_type, LogType::Default);
1044        assert_eq!(results[0].event_type, EventType::Log);
1045        assert_eq!(results[0].euid, 0);
1046        assert_eq!(results[0].boot_uuid, "80D194AF56A34C54867449D2130D41BB");
1047        assert_eq!(results[0].timezone_name, "Pacific");
1048        assert_eq!(results[0].raw_message, "LOMD Start");
1049        assert_eq!(results[0].timestamp, "2022-01-16T03:05:26.434850816Z")
1050    }
1051
1052    #[test]
1053    fn test_get_log_type() {
1054        let mut log_type = 0x2;
1055        let activity_type = 0x2;
1056
1057        let mut log_string = LogData::get_log_type(log_type, activity_type);
1058        assert_eq!(log_string, LogType::Debug);
1059        log_type = 0x1;
1060        log_string = LogData::get_log_type(log_type, activity_type);
1061        assert_eq!(log_string, LogType::Create);
1062    }
1063
1064    #[test]
1065    fn test_get_event_type() {
1066        let event_type = 0x2;
1067        let event_string = LogData::get_event_type(event_type);
1068        assert_eq!(event_string, EventType::Activity);
1069    }
1070
1071    #[test]
1072    fn test_get_header_data() {
1073        let test_chunk_header = [
1074            0, 16, 0, 0, 17, 0, 0, 0, 208, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 15, 105,
1075            217, 162, 204, 126, 0, 0, 48, 215, 18, 98, 0, 0, 0, 0, 203, 138, 9, 0, 44, 1, 0, 0, 0,
1076            0, 0, 0, 1, 0, 0, 0, 0, 97, 0, 0, 8, 0, 0, 0, 6, 112, 124, 198, 169, 153, 1, 0, 1, 97,
1077            0, 0, 56, 0, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 50, 49, 65, 53, 53, 57, 0, 0, 0, 0, 0, 0, 0,
1078            0, 0, 0, 77, 97, 99, 66, 111, 111, 107, 80, 114, 111, 49, 54, 44, 49, 0, 0, 0, 0, 0, 0,
1079            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 97, 0, 0, 24, 0, 0, 0, 195, 32, 184, 206, 151,
1080            250, 77, 165, 159, 49, 125, 57, 46, 56, 156, 234, 85, 0, 0, 0, 0, 0, 0, 0, 3, 97, 0, 0,
1081            48, 0, 0, 0, 47, 118, 97, 114, 47, 100, 98, 47, 116, 105, 109, 101, 122, 111, 110, 101,
1082            47, 122, 111, 110, 101, 105, 110, 102, 111, 47, 65, 109, 101, 114, 105, 99, 97, 47, 78,
1083            101, 119, 95, 89, 111, 114, 107, 0, 0, 0, 0, 0, 0,
1084        ];
1085        let mut data = UnifiedLogData {
1086            header: Vec::new(),
1087            catalog_data: Vec::new(),
1088            oversize: Vec::new(),
1089        };
1090
1091        LogData::get_header_data(&test_chunk_header, &mut data);
1092        assert_eq!(data.header.len(), 1);
1093    }
1094
1095    #[test]
1096    fn test_get_catalog_data() {
1097        let test_chunk_catalog = [
1098            11, 96, 0, 0, 17, 0, 0, 0, 208, 1, 0, 0, 0, 0, 0, 0, 32, 0, 96, 0, 1, 0, 160, 0, 7, 0,
1099            0, 0, 0, 0, 0, 0, 20, 165, 44, 35, 253, 233, 2, 0, 43, 239, 210, 12, 24, 236, 56, 56,
1100            129, 79, 43, 78, 90, 243, 188, 236, 61, 5, 132, 95, 63, 101, 53, 143, 158, 191, 34, 54,
1101            231, 114, 172, 1, 99, 111, 109, 46, 97, 112, 112, 108, 101, 46, 83, 107, 121, 76, 105,
1102            103, 104, 116, 0, 112, 101, 114, 102, 111, 114, 109, 97, 110, 99, 101, 95, 105, 110,
1103            115, 116, 114, 117, 109, 101, 110, 116, 97, 116, 105, 111, 110, 0, 116, 114, 97, 99,
1104            105, 110, 103, 46, 115, 116, 97, 108, 108, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 158,
1105            0, 0, 0, 0, 0, 0, 0, 55, 1, 0, 0, 158, 0, 0, 0, 88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1106            0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 87, 0, 0, 0, 19, 0, 78, 0, 0, 0, 47, 0, 0, 0, 0, 0,
1107            246, 113, 118, 43, 250, 233, 2, 0, 62, 195, 90, 26, 9, 234, 2, 0, 120, 255, 0, 0, 0, 1,
1108            0, 0, 1, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 19, 0, 47, 0, 48, 89, 60, 28, 9, 234, 2, 0,
1109            99, 50, 207, 40, 18, 234, 2, 0, 112, 240, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 3, 0, 0,
1110            0, 0, 0, 19, 0, 47, 0, 153, 6, 208, 41, 18, 234, 2, 0, 0, 214, 108, 78, 32, 234, 2, 0,
1111            0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 19, 0, 47, 0, 128, 0, 87,
1112            79, 32, 234, 2, 0, 137, 5, 2, 205, 41, 234, 2, 0, 88, 255, 0, 0, 0, 1, 0, 0, 1, 0, 0,
1113            0, 0, 0, 3, 0, 0, 0, 0, 0, 19, 0, 47, 0, 185, 11, 2, 205, 41, 234, 2, 0, 172, 57, 107,
1114            20, 56, 234, 2, 0, 152, 255, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 19,
1115            0, 47, 0, 53, 172, 105, 21, 56, 234, 2, 0, 170, 167, 194, 43, 68, 234, 2, 0, 144, 255,
1116            0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 19, 0, 47, 0, 220, 202, 171, 57,
1117            68, 234, 2, 0, 119, 171, 170, 119, 76, 234, 2, 0, 240, 254, 0, 0, 0, 1, 0, 0, 1, 0, 0,
1118            0, 0, 0, 3, 0, 0, 0, 0, 0, 19, 0, 47, 0,
1119        ];
1120        let mut data = UnifiedLogCatalogData::default();
1121
1122        LogData::get_catalog_data(&test_chunk_catalog, &mut data);
1123        assert_eq!(data.catalog.chunk_tag, 0x600b);
1124        assert_eq!(data.catalog.chunk_sub_tag, 17);
1125        assert_eq!(data.catalog.chunk_data_size, 464);
1126        assert_eq!(data.catalog.catalog_subsystem_strings_offset, 32);
1127        assert_eq!(data.catalog.catalog_process_info_entries_offset, 96);
1128        assert_eq!(data.catalog.number_process_information_entries, 1);
1129        assert_eq!(data.catalog.catalog_offset_sub_chunks, 160);
1130        assert_eq!(data.catalog.number_sub_chunks, 7);
1131        assert_eq!(data.catalog.unknown, [0, 0, 0, 0, 0, 0]);
1132        assert_eq!(data.catalog.earliest_firehose_timestamp, 820223379547412);
1133        assert_eq!(
1134            data.catalog.catalog_uuids,
1135            [
1136                "2BEFD20C18EC3838814F2B4E5AF3BCEC",
1137                "3D05845F3F65358F9EBF2236E772AC01"
1138            ]
1139        );
1140        assert_eq!(
1141            data.catalog.catalog_subsystem_strings,
1142            [
1143                99, 111, 109, 46, 97, 112, 112, 108, 101, 46, 83, 107, 121, 76, 105, 103, 104, 116,
1144                0, 112, 101, 114, 102, 111, 114, 109, 97, 110, 99, 101, 95, 105, 110, 115, 116,
1145                114, 117, 109, 101, 110, 116, 97, 116, 105, 111, 110, 0, 116, 114, 97, 99, 105,
1146                110, 103, 46, 115, 116, 97, 108, 108, 115, 0, 0, 0
1147            ]
1148        );
1149        assert_eq!(data.catalog.catalog_process_info_entries.len(), 1);
1150        assert_eq!(
1151            data.catalog
1152                .catalog_process_info_entries
1153                .get("158_311")
1154                .unwrap()
1155                .main_uuid,
1156            "2BEFD20C18EC3838814F2B4E5AF3BCEC"
1157        );
1158        assert_eq!(
1159            data.catalog
1160                .catalog_process_info_entries
1161                .get("158_311")
1162                .unwrap()
1163                .dsc_uuid,
1164            "3D05845F3F65358F9EBF2236E772AC01"
1165        );
1166
1167        assert_eq!(data.catalog.catalog_subchunks.len(), 7)
1168    }
1169
1170    #[test]
1171    fn test_get_chunkset_data() {
1172        let mut test_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1173        test_path.push("tests/test_data/Chunkset Tests/high_sierra_compressed_chunkset.raw");
1174
1175        let buffer = fs::read(test_path).unwrap();
1176
1177        let mut unified_log = UnifiedLogCatalogData::default();
1178
1179        let mut log_data = UnifiedLogData::default();
1180
1181        LogData::get_chunkset_data(&buffer, &mut unified_log, &mut log_data);
1182        assert_eq!(unified_log.catalog.chunk_tag, 0);
1183        assert_eq!(unified_log.firehose.len(), 21);
1184        assert_eq!(unified_log.statedump.len(), 0);
1185        assert_eq!(unified_log.simpledump.len(), 0);
1186        assert_eq!(unified_log.oversize.len(), 0);
1187
1188        assert_eq!(
1189            unified_log.firehose[0].public_data[0].message.item_info[0].message_strings,
1190            "483.700"
1191        );
1192        assert_eq!(unified_log.firehose[0].base_continous_time, 0);
1193        assert_eq!(unified_log.firehose[0].first_number_proc_id, 70);
1194        assert_eq!(unified_log.firehose[0].second_number_proc_id, 71);
1195        assert_eq!(unified_log.firehose[0].public_data_size, 4040);
1196        assert_eq!(unified_log.firehose[0].private_data_virtual_offset, 4096);
1197    }
1198
1199    #[test]
1200    fn test_track_missing() {
1201        let first_proc_id = 1;
1202        let second_proc_id = 2;
1203        let time = 11;
1204        let test_firehose = Firehose::default();
1205
1206        let missing_firehose =
1207            LogData::track_missing(first_proc_id, second_proc_id, time, test_firehose);
1208        assert_eq!(missing_firehose.first_number_proc_id, first_proc_id);
1209        assert_eq!(missing_firehose.second_number_proc_id, second_proc_id);
1210        assert_eq!(missing_firehose.base_continous_time, time);
1211    }
1212
1213    #[test]
1214    fn test_add_missing() {
1215        let mut missing_unified_log_data_vec = UnifiedLogData {
1216            header: Vec::new(),
1217            catalog_data: Vec::new(),
1218            oversize: Vec::new(),
1219        };
1220        let mut test_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1221        test_path.push(
1222            "tests/test_data/system_logs_big_sur.logarchive/Persist/0000000000000002.tracev3",
1223        );
1224        let reader = std::fs::File::open(test_path).unwrap();
1225
1226        let log_data = parse_log(reader).unwrap();
1227
1228        LogData::add_missing(
1229            &log_data.catalog_data[0],
1230            0,
1231            0,
1232            &log_data.header,
1233            &mut missing_unified_log_data_vec,
1234            &log_data.catalog_data[0].firehose[0],
1235        );
1236        assert_eq!(missing_unified_log_data_vec.header.len(), 1);
1237        assert_eq!(
1238            missing_unified_log_data_vec.header[0].boot_uuid,
1239            "80D194AF56A34C54867449D2130D41BB"
1240        );
1241        assert_eq!(missing_unified_log_data_vec.header[0].logd_pid, 42);
1242        assert_eq!(missing_unified_log_data_vec.catalog_data.len(), 1);
1243        assert_eq!(
1244            missing_unified_log_data_vec.catalog_data[0]
1245                .catalog
1246                .catalog_subsystem_strings_offset,
1247            848
1248        );
1249        assert_eq!(
1250            missing_unified_log_data_vec.catalog_data[0].firehose.len(),
1251            1
1252        );
1253        assert_eq!(
1254            missing_unified_log_data_vec.catalog_data[0].firehose[0].first_number_proc_id,
1255            45
1256        );
1257        assert_eq!(
1258            missing_unified_log_data_vec.catalog_data[0].firehose[0].second_number_proc_id,
1259            188
1260        );
1261    }
1262}