1use crate::dsc::SharedCacheStrings;
2use crate::traits::{FileProvider, SourceFile};
3use crate::uuidtext::UUIDText;
4use log::error;
5use std::collections::HashMap;
6use std::fs::File;
7use std::io::{Error, ErrorKind};
8use std::path::{Component, Path, PathBuf};
9use walkdir::WalkDir;
10
11pub struct LocalFile {
12 reader: File,
13 source: String,
14}
15
16impl LocalFile {
17 fn new(path: &Path) -> std::io::Result<Self> {
18 Ok(Self {
19 reader: File::open(path)?,
20 source: path.as_os_str().to_string_lossy().to_string(),
21 })
22 }
23}
24
25impl SourceFile for LocalFile {
26 fn reader(&mut self) -> Box<&mut dyn std::io::Read> {
27 Box::new(&mut self.reader)
28 }
29
30 fn source_path(&self) -> &str {
31 self.source.as_str()
32 }
33}
34
35#[derive(Default, Debug)]
45pub struct LiveSystemProvider {
46 pub(crate) uuidtext_cache: HashMap<String, UUIDText>,
47 pub(crate) dsc_cache: HashMap<String, SharedCacheStrings>,
48}
49
50impl LiveSystemProvider {
51 pub fn new() -> Self {
52 Self {
53 uuidtext_cache: HashMap::new(),
54 dsc_cache: HashMap::new(),
55 }
56 }
57}
58
59static TRACE_FOLDERS: &[&str] = &["HighVolume", "Special", "Signpost", "Persist"];
60
61#[derive(Debug, PartialEq)]
62pub enum LogFileType {
63 TraceV3,
64 UUIDText,
65 Dsc,
66 Timesync,
67 Invalid,
68}
69
70fn only_hex_chars(val: &str) -> bool {
71 val.chars().all(|c| c.is_ascii_hexdigit())
72}
73
74impl From<&Path> for LogFileType {
75 fn from(path: &Path) -> Self {
76 let components = path.components().collect::<Vec<Component<'_>>>();
77 let n = components.len();
78
79 if let (Some(&Component::Normal(parent)), Some(&Component::Normal(filename))) =
80 (components.get(n - 2), components.get(n - 1))
81 {
82 let parent_s = parent.to_str().unwrap_or_default();
83 let filename_s = filename.to_str().unwrap_or_default();
84
85 if filename_s == "logdata.LiveData.tracev3"
86 || (filename_s.ends_with(".tracev3") && TRACE_FOLDERS.contains(&parent_s))
87 {
88 return Self::TraceV3;
89 }
90
91 if filename_s.len() == 30
92 && only_hex_chars(filename_s)
93 && parent_s.len() == 2
94 && only_hex_chars(parent_s)
95 {
96 return Self::UUIDText;
97 }
98
99 if filename_s.len() == 32 && only_hex_chars(filename_s) && parent_s == "dsc" {
100 return Self::Dsc;
101 }
102
103 if filename_s.ends_with(".timesync") && parent_s == "timesync" {
104 return Self::Timesync;
105 }
106 }
107
108 Self::Invalid
109 }
110}
111
112impl FileProvider for LiveSystemProvider {
113 fn tracev3_files(&self) -> Box<dyn Iterator<Item = Box<dyn SourceFile>>> {
114 let path = PathBuf::from("/private/var/db/diagnostics");
115 Box::new(
116 WalkDir::new(path)
117 .sort_by(|a, b| a.file_name().cmp(b.file_name()))
118 .into_iter()
119 .filter_map(|entry| entry.ok())
120 .filter(|entry| matches!(LogFileType::from(entry.path()), LogFileType::TraceV3))
121 .filter_map(|entry| {
122 Some(Box::new(LocalFile::new(entry.path()).ok()?) as Box<dyn SourceFile>)
123 }),
124 )
125 }
126
127 fn uuidtext_files(&self) -> Box<dyn Iterator<Item = Box<dyn SourceFile>>> {
128 let path = PathBuf::from("/private/var/db/uuidtext");
129 Box::new(
130 WalkDir::new(path)
131 .into_iter()
132 .filter_map(|entry| entry.ok())
133 .filter(|entry| matches!(LogFileType::from(entry.path()), LogFileType::UUIDText))
134 .filter_map(|entry| {
135 Some(Box::new(LocalFile::new(entry.path()).ok()?) as Box<dyn SourceFile>)
136 }),
137 )
138 }
139
140 fn read_uuidtext(&self, uuid: &str) -> Result<UUIDText, Error> {
141 let uuid_len = 32;
142 let uuid = if uuid.len() == uuid_len - 1 {
143 &format!("0{uuid}")
145 } else if uuid.len() == uuid_len - 2 {
146 &format!("00{uuid}")
148 } else if uuid.len() == uuid_len {
149 uuid
150 } else {
151 return Err(Error::new(
152 ErrorKind::NotFound,
153 format!("uuid length not correct: {uuid}"),
154 ));
155 };
156
157 let dir_name = format!("{}{}", &uuid[0..1], &uuid[1..2]);
158 let filename = &uuid[2..];
159
160 let mut path = PathBuf::from("/private/var/db/uuidtext");
161
162 path.push(dir_name);
163 path.push(filename);
164
165 let mut buf = Vec::new();
166 let mut file = LocalFile::new(&path)?;
167 file.reader().read_to_end(&mut buf)?;
168
169 let uuid_text = match UUIDText::parse_uuidtext(&buf) {
170 Ok((_, results)) => results,
171 Err(err) => {
172 error!(
173 "[macos-unifiedlogs] Failed to parse UUID file {}: {err:?}",
174 path.to_str().unwrap_or_default()
175 );
176 return Err(Error::new(
177 ErrorKind::InvalidData,
178 format!("failed to read: {uuid}"),
179 ));
180 }
181 };
182
183 Ok(uuid_text)
184 }
185
186 fn cached_uuidtext(&self, uuid: &str) -> Option<&UUIDText> {
187 self.uuidtext_cache.get(uuid)
188 }
189
190 fn update_uuid(&mut self, uuid: &str, uuid2: &str) {
191 let status = match self.read_uuidtext(uuid) {
192 Ok(result) => result,
193 Err(_err) => return,
194 };
195 if self.uuidtext_cache.len() > 30 {
197 for key in self
198 .uuidtext_cache
199 .keys()
200 .take(5)
201 .cloned()
202 .collect::<Vec<String>>()
203 {
204 if key == uuid || key == uuid2 {
205 continue;
206 }
207 let key = key.clone();
208 self.uuidtext_cache.remove(&key);
209 }
210 }
211 self.uuidtext_cache.insert(uuid.to_string(), status);
212 }
213
214 fn update_dsc(&mut self, uuid: &str, uuid2: &str) {
215 let status = match self.read_dsc_uuid(uuid) {
216 Ok(result) => result,
217 Err(_err) => return,
218 };
219 while self.dsc_cache.len() > 2 {
222 if let Some(key) = self.dsc_cache.keys().next() {
223 if key == uuid || key == uuid2 {
224 continue;
225 }
226 let key = key.clone();
227 self.dsc_cache.remove(&key);
228 }
229 }
230 self.dsc_cache.insert(uuid.to_string(), status);
231 }
232
233 fn cached_dsc(&self, uuid: &str) -> Option<&SharedCacheStrings> {
234 self.dsc_cache.get(uuid)
235 }
236
237 fn read_dsc_uuid(&self, uuid: &str) -> Result<SharedCacheStrings, Error> {
238 let uuid_len = 32;
239 let uuid = if uuid.len() == uuid_len - 1 {
240 &format!("0{uuid}")
242 } else if uuid.len() == uuid_len - 2 {
243 &format!("00{uuid}")
245 } else if uuid.len() == uuid_len {
246 uuid
247 } else {
248 return Err(Error::new(
249 ErrorKind::NotFound,
250 format!("uuid length not correct: {uuid}"),
251 ));
252 };
253
254 let mut path = PathBuf::from("/private/var/db/uuidtext/dsc");
255 path.push(uuid);
256
257 let mut buf = Vec::new();
258 let mut file = LocalFile::new(&path)?;
259 file.reader().read_to_end(&mut buf)?;
260
261 let uuid_text = match SharedCacheStrings::parse_dsc(&buf) {
262 Ok((_, results)) => results,
263 Err(err) => {
264 error!(
265 "[macos-unifiedlogs] Failed to parse dsc UUID file {}: {err:?}",
266 path.to_str().unwrap_or_default(),
267 );
268 return Err(Error::new(
269 ErrorKind::InvalidData,
270 format!("failed to read: {uuid}"),
271 ));
272 }
273 };
274
275 Ok(uuid_text)
276 }
277
278 fn dsc_files(&self) -> Box<dyn Iterator<Item = Box<dyn SourceFile>>> {
279 let path = PathBuf::from("/private/var/db/uuidtext/dsc");
280 Box::new(WalkDir::new(path).into_iter().filter_map(|entry| {
281 if !matches!(
282 LogFileType::from(entry.as_ref().ok()?.path()),
283 LogFileType::Dsc
284 ) {
285 return None;
286 }
287 Some(Box::new(LocalFile::new(entry.ok()?.path()).ok()?) as Box<dyn SourceFile>)
288 }))
289 }
290
291 fn timesync_files(&self) -> Box<dyn Iterator<Item = Box<dyn SourceFile>>> {
292 let path = PathBuf::from("/private/var/db/diagnostics/timesync");
293 Box::new(
294 WalkDir::new(path)
295 .into_iter()
296 .filter_map(|entry| entry.ok())
297 .filter(|entry| matches!(LogFileType::from(entry.path()), LogFileType::Timesync))
298 .filter_map(|entry| {
299 Some(Box::new(LocalFile::new(entry.path()).ok()?) as Box<dyn SourceFile>)
300 }),
301 )
302 }
303}
304
305pub struct LogarchiveProvider {
317 base: PathBuf,
318 pub(crate) uuidtext_cache: HashMap<String, UUIDText>,
319 pub(crate) dsc_cache: HashMap<String, SharedCacheStrings>,
320}
321
322impl LogarchiveProvider {
323 pub fn new(path: &Path) -> Self {
324 Self {
325 base: path.to_path_buf(),
326 uuidtext_cache: HashMap::new(),
327 dsc_cache: HashMap::new(),
328 }
329 }
330}
331
332impl FileProvider for LogarchiveProvider {
333 fn tracev3_files(&self) -> Box<dyn Iterator<Item = Box<dyn SourceFile>>> {
349 Box::new(
350 WalkDir::new(&self.base)
351 .sort_by(|a, b| a.file_name().cmp(b.file_name()))
352 .into_iter()
353 .filter_map(|entry| entry.ok())
354 .filter(|entry| matches!(LogFileType::from(entry.path()), LogFileType::TraceV3))
355 .filter_map(|entry| {
356 Some(Box::new(LocalFile::new(entry.path()).ok()?) as Box<dyn SourceFile>)
357 }),
358 )
359 }
360
361 fn uuidtext_files(&self) -> Box<dyn Iterator<Item = Box<dyn SourceFile>>> {
362 Box::new(
363 WalkDir::new(&self.base)
364 .into_iter()
365 .filter_map(|entry| entry.ok())
366 .filter(|entry| matches!(LogFileType::from(entry.path()), LogFileType::UUIDText))
367 .filter_map(|entry| {
368 Some(Box::new(LocalFile::new(entry.path()).ok()?) as Box<dyn SourceFile>)
369 }),
370 )
371 }
372
373 fn read_uuidtext(&self, uuid: &str) -> Result<UUIDText, Error> {
374 let uuid_len = 32;
375 let uuid = if uuid.len() == uuid_len - 1 {
376 &format!("0{uuid}")
378 } else if uuid.len() == uuid_len - 2 {
379 &format!("00{uuid}")
381 } else if uuid.len() == uuid_len {
382 uuid
383 } else {
384 return Err(Error::new(
385 ErrorKind::NotFound,
386 format!("uuid length not correct: {uuid}"),
387 ));
388 };
389
390 let dir_name = format!("{}{}", &uuid[0..1], &uuid[1..2]);
391 let filename = &uuid[2..];
392
393 let mut base = self.base.clone();
394 base.push(dir_name);
395 base.push(filename);
396
397 let mut buf = Vec::new();
398 let mut file = LocalFile::new(&base)?;
399 file.reader().read_to_end(&mut buf)?;
400
401 let uuid_text = match UUIDText::parse_uuidtext(&buf) {
402 Ok((_, results)) => results,
403 Err(err) => {
404 error!(
405 "[macos-unifiedlogs] Failed to parse UUID file {}: {err:?}",
406 base.to_str().unwrap_or_default(),
407 );
408 return Err(Error::new(
409 ErrorKind::InvalidData,
410 format!("failed to read: {uuid}"),
411 ));
412 }
413 };
414
415 Ok(uuid_text)
416 }
417
418 fn read_dsc_uuid(&self, uuid: &str) -> Result<SharedCacheStrings, Error> {
419 let uuid_len = 32;
420 let uuid = if uuid.len() == uuid_len - 1 {
421 &format!("0{uuid}")
423 } else if uuid.len() == uuid_len - 2 {
424 &format!("00{uuid}")
426 } else if uuid.len() == uuid_len {
427 uuid
428 } else {
429 return Err(Error::new(
430 ErrorKind::NotFound,
431 format!("uuid length not correct: {uuid}"),
432 ));
433 };
434
435 let mut base = self.base.clone();
436 base.push("dsc");
437 base.push(uuid);
438
439 let mut buf = Vec::new();
440 let mut file = LocalFile::new(&base)?;
441 file.reader().read_to_end(&mut buf)?;
442
443 let uuid_text = match SharedCacheStrings::parse_dsc(&buf) {
444 Ok((_, results)) => results,
445 Err(err) => {
446 error!(
447 "[macos-unifiedlogs] Failed to parse dsc UUID file {}: {err:?}",
448 base.to_str().unwrap_or_default(),
449 );
450 return Err(Error::new(
451 ErrorKind::InvalidData,
452 format!("failed to read: {uuid}"),
453 ));
454 }
455 };
456
457 Ok(uuid_text)
458 }
459
460 fn cached_uuidtext(&self, uuid: &str) -> Option<&UUIDText> {
461 self.uuidtext_cache.get(uuid)
462 }
463
464 fn cached_dsc(&self, uuid: &str) -> Option<&SharedCacheStrings> {
465 self.dsc_cache.get(uuid)
466 }
467
468 fn dsc_files(&self) -> Box<dyn Iterator<Item = Box<dyn SourceFile>>> {
469 Box::new(
470 WalkDir::new(&self.base)
471 .into_iter()
472 .filter_map(|entry| entry.ok())
473 .filter(|entry| matches!(LogFileType::from(entry.path()), LogFileType::Dsc))
474 .filter_map(|entry| {
475 Some(Box::new(LocalFile::new(entry.path()).ok()?) as Box<dyn SourceFile>)
476 }),
477 )
478 }
479
480 fn update_uuid(&mut self, uuid: &str, uuid2: &str) {
481 let status = match self.read_uuidtext(uuid) {
482 Ok(result) => result,
483 Err(_err) => return,
484 };
485 if self.uuidtext_cache.len() > 30 {
487 for key in self
488 .uuidtext_cache
489 .keys()
490 .take(5)
491 .cloned()
492 .collect::<Vec<String>>()
493 {
494 if key == uuid || key == uuid2 {
495 continue;
496 }
497 let key = key.clone();
498 self.uuidtext_cache.remove(&key);
499 }
500 }
501 self.uuidtext_cache.insert(uuid.to_string(), status);
502 }
503
504 fn update_dsc(&mut self, uuid: &str, uuid2: &str) {
505 let status = match self.read_dsc_uuid(uuid) {
506 Ok(result) => result,
507 Err(_err) => return,
508 };
509 while self.dsc_cache.len() > 2 {
512 if let Some(key) = self.dsc_cache.keys().next() {
513 if key == uuid || key == uuid2 {
514 continue;
515 }
516 let key = key.clone();
517 self.dsc_cache.remove(&key);
518 }
519 }
520 self.dsc_cache.insert(uuid.to_string(), status);
521 }
522
523 fn timesync_files(&self) -> Box<dyn Iterator<Item = Box<dyn SourceFile>>> {
524 Box::new(
525 WalkDir::new(&self.base)
526 .into_iter()
527 .filter_map(|entry| entry.ok())
528 .filter(|entry| matches!(LogFileType::from(entry.path()), LogFileType::Timesync))
529 .filter_map(|entry| {
530 Some(Box::new(LocalFile::new(entry.path()).ok()?) as Box<dyn SourceFile>)
531 }),
532 )
533 }
534}
535
536#[cfg(test)]
537mod tests {
538 use super::{LogFileType, LogarchiveProvider};
539 use crate::traits::FileProvider;
540 use std::path::PathBuf;
541
542 #[test]
543 fn test_only_hex() {
544 use super::only_hex_chars;
545
546 let cases = vec![
547 "A7563E1D7A043ED29587044987205172",
548 "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD",
549 ];
550
551 for case in cases {
552 assert!(only_hex_chars(case));
553 }
554 }
555
556 #[test]
557 fn test_validate_uuidtext_path() {
558 let valid_cases = vec![
559 "/private/var/db/uuidtext/dsc/A7563E1D7A043ED29587044987205172",
560 "/private/var/db/uuidtext/dsc/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD",
561 "./dsc/A7563E1D7A043ED29587044987B05172",
562 ];
563
564 for case in valid_cases {
565 let path = PathBuf::from(case);
566 let file_type = LogFileType::from(path.as_path());
567 assert_eq!(file_type, LogFileType::Dsc);
568 }
569 }
570
571 #[test]
572 fn test_read_uuidtext() {
573 let mut test_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
574 test_path.push("tests/test_data/system_logs_big_sur.logarchive");
575 let provider = LogarchiveProvider::new(test_path.as_path());
576 let uuid = provider
577 .read_uuidtext("25A8CFC3A9C035F19DBDC16F994EA948")
578 .unwrap();
579 assert_eq!(uuid.entry_descriptors.len(), 2);
580 assert_eq!(uuid.uuid, "");
581 assert_eq!(uuid.footer_data.len(), 76544);
582 assert_eq!(uuid.signature, 1719109785);
583 assert_eq!(uuid.unknown_major_version, 2);
584 assert_eq!(uuid.unknown_minor_version, 1);
585 assert_eq!(uuid.number_entries, 2);
586 }
587
588 #[test]
589 fn test_read_dsc_uuid() {
590 let mut test_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
591 test_path.push("tests/test_data/system_logs_big_sur.logarchive");
592 let provider = LogarchiveProvider::new(test_path.as_path());
593 let uuid = provider
594 .read_dsc_uuid("80896B329EB13A10A7C5449B15305DE2")
595 .unwrap();
596 assert_eq!(uuid.dsc_uuid, "");
597 assert_eq!(uuid.major_version, 1);
598 assert_eq!(uuid.minor_version, 0);
599 assert_eq!(uuid.number_ranges, 2993);
600 assert_eq!(uuid.number_uuids, 1976);
601 assert_eq!(uuid.ranges.len(), 2993);
602 assert_eq!(uuid.uuids.len(), 1976);
603 assert_eq!(uuid.signature, 1685283688);
604 }
605
606 #[test]
607 fn test_validate_dsc_path() {}
608
609 #[test]
610 fn test_validate_timesync_path() {}
611
612 #[test]
613 fn test_validate_tracev3_path() {}
614}