macos_unifiedlogs/
iterator.rs

1use crate::{
2    header::HeaderChunk,
3    preamble::LogPreamble,
4    unified_log::{LogData, UnifiedLogCatalogData, UnifiedLogData},
5    util::padding_size_8,
6};
7use log::{error, warn};
8use nom::bytes::complete::take;
9
10#[derive(Debug, Clone)]
11/// Iterator to loop through Chunks in the tracev3 file
12pub struct UnifiedLogIterator {
13    pub data: Vec<u8>,
14    pub header: Vec<HeaderChunk>,
15}
16
17impl Iterator for UnifiedLogIterator {
18    type Item = UnifiedLogData;
19    fn next(&mut self) -> Option<Self::Item> {
20        if self.data.is_empty() {
21            return None;
22        }
23        let mut unified_log_data_true = UnifiedLogData {
24            header: self.header.clone(),
25            catalog_data: Vec::new(),
26            oversize: Vec::new(),
27        };
28
29        let mut catalog_data = UnifiedLogCatalogData::default();
30
31        let mut input = self.data.as_slice();
32        let chunk_preamble_size = 16; // Include preamble size in total chunk size
33
34        let header_chunk = 0x1000;
35        let catalog_chunk = 0x600b;
36        let chunkset_chunk = 0x600d;
37
38        loop {
39            let preamble_result = LogPreamble::detect_preamble(input);
40            let preamble = match preamble_result {
41                Ok((_, result)) => result,
42                Err(_err) => {
43                    error!("Failed to determine preamble chunk");
44                    return None;
45                }
46            };
47            let chunk_size = preamble.chunk_data_size;
48
49            // Grab all data associated with Unified Log entry (chunk)
50            let chunk_result = nom_bytes(input, &(chunk_size + chunk_preamble_size));
51
52            let (data, chunk_data) = match chunk_result {
53                Ok(result) => result,
54                Err(_err) => {
55                    error!("Failed to nom chunk bytes");
56                    return None;
57                }
58            };
59
60            if preamble.chunk_tag == header_chunk {
61                LogData::get_header_data(chunk_data, &mut unified_log_data_true);
62            } else if preamble.chunk_tag == catalog_chunk {
63                if catalog_data.catalog.chunk_tag != 0 {
64                    self.data = input.to_vec();
65                    break;
66                }
67
68                LogData::get_catalog_data(chunk_data, &mut catalog_data);
69            } else if preamble.chunk_tag == chunkset_chunk {
70                LogData::get_chunkset_data(
71                    chunk_data,
72                    &mut catalog_data,
73                    &mut unified_log_data_true,
74                );
75            } else {
76                error!(
77                    "[macos-unifiedlogs] Unknown chunk type: {}",
78                    preamble.chunk_tag
79                );
80            }
81
82            let padding_size = padding_size_8(preamble.chunk_data_size);
83            if self.data.len() < padding_size as usize {
84                self.data = Vec::new();
85                break;
86            }
87            let data_result = nom_bytes(data, &padding_size);
88            let data = match data_result {
89                Ok((result, _)) => result,
90                Err(_err) => {
91                    error!("Failed to nom log end padding");
92                    return None;
93                }
94            };
95            if data.is_empty() {
96                self.data = Vec::new();
97                break;
98            }
99            input = data;
100            if input.len() < chunk_preamble_size as usize {
101                warn!(
102                    "Not enough data for preamble header, needed 16 bytes. Got: {}",
103                    input.len()
104                );
105                self.data = Vec::new();
106                break;
107            }
108        }
109
110        // Make sure to get the last catalog
111        if catalog_data.catalog.chunk_tag != 0 {
112            unified_log_data_true.catalog_data.push(catalog_data);
113        }
114        self.header = unified_log_data_true.header.clone();
115        Some(unified_log_data_true)
116    }
117}
118
119/// Nom bytes of the log chunk
120fn nom_bytes<'a>(data: &'a [u8], size: &u64) -> nom::IResult<&'a [u8], &'a [u8]> {
121    let size = match usize::try_from(*size).ok() {
122        Some(s) => s,
123        None => {
124            error!("[macos-unifiedlogs] u64 is bigger than system usize");
125            return Err(nom::Err::Error(nom::error::Error::new(
126                data,
127                nom::error::ErrorKind::TooLarge,
128            )));
129        }
130    };
131    take(size)(data)
132}
133
134#[cfg(test)]
135mod tests {
136    use super::UnifiedLogIterator;
137    use crate::{
138        filesystem::LogarchiveProvider,
139        iterator::nom_bytes,
140        parser::{build_log, collect_timesync},
141        unified_log::{EventType, LogType},
142    };
143    use std::{fs, path::PathBuf};
144
145    #[test]
146    fn test_unified_log_iterator() {
147        let mut test_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
148        test_path.push("tests/test_data/system_logs_big_sur.logarchive");
149
150        test_path.push("Persist/0000000000000002.tracev3");
151        let buffer_results = fs::read(test_path.to_str().unwrap()).unwrap();
152
153        let log_iterator = UnifiedLogIterator {
154            data: buffer_results,
155            header: Vec::new(),
156        };
157
158        let mut total = 0;
159        for chunk in log_iterator {
160            if chunk.catalog_data[0].firehose.len() == 99 {
161                assert_eq!(chunk.catalog_data[0].firehose.len(), 99);
162                assert_eq!(chunk.catalog_data[0].simpledump.len(), 0);
163                assert_eq!(chunk.header.len(), 1);
164                assert!(
165                    chunk.catalog_data[0]
166                        .catalog
167                        .catalog_process_info_entries
168                        .len()
169                        > 40
170                );
171                assert_eq!(chunk.catalog_data[0].statedump.len(), 0);
172            }
173
174            total += chunk.catalog_data.len();
175        }
176
177        assert_eq!(total, 56);
178    }
179
180    #[test]
181    fn test_unified_log_iterator_build_log() {
182        let mut test_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
183        test_path.push("tests/test_data/system_logs_big_sur.logarchive");
184
185        let mut provider = LogarchiveProvider::new(test_path.as_path());
186        let timesync_data = collect_timesync(&provider).unwrap();
187
188        test_path.push("Persist/0000000000000002.tracev3");
189        let buffer_results = fs::read(test_path.to_str().unwrap()).unwrap();
190
191        let log_iterator = UnifiedLogIterator {
192            data: buffer_results,
193            header: Vec::new(),
194        };
195
196        let mut total = 0;
197        for chunk in log_iterator {
198            let exclude_missing = false;
199            let (results, _) = build_log(&chunk, &mut provider, &timesync_data, exclude_missing);
200
201            if results[10].time == 1642302327364384800.0 {
202                assert_eq!(results.len(), 3805);
203                assert_eq!(results[10].process, "/usr/libexec/lightsoutmanagementd");
204                assert_eq!(results[10].subsystem, "com.apple.lom");
205                assert_eq!(results[10].time, 1642302327364384800.0);
206                assert_eq!(results[10].activity_id, 0);
207                assert_eq!(
208                    results[10].library,
209                    "/System/Library/PrivateFrameworks/AppleLOM.framework/Versions/A/AppleLOM"
210                );
211                assert_eq!(results[10].message, "<private> LOM isSupported : No");
212                assert_eq!(results[10].pid, 45);
213                assert_eq!(results[10].thread_id, 588);
214                assert_eq!(results[10].category, "device");
215                assert_eq!(results[10].log_type, LogType::Default);
216                assert_eq!(results[10].event_type, EventType::Log);
217                assert_eq!(results[10].euid, 0);
218                assert_eq!(results[10].boot_uuid, "80D194AF56A34C54867449D2130D41BB");
219                assert_eq!(results[10].timezone_name, "Pacific");
220                assert_eq!(results[10].library_uuid, "D8E5AF1CAF4F3CEB8731E6F240E8EA7D");
221                assert_eq!(results[10].process_uuid, "6C3ADF991F033C1C96C4ADFAA12D8CED");
222                assert_eq!(results[10].raw_message, "%@ LOM isSupported : %s");
223            }
224
225            total += results.len();
226        }
227
228        assert_eq!(total, 207366);
229    }
230
231    #[test]
232    fn test_nom_bytes() {
233        let test = [1, 0, 0, 0];
234        let (left, _) = nom_bytes(&test, &1).unwrap();
235        assert_eq!(left.len(), 3);
236    }
237}