1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::fmt;
4use std::io::{self, BufRead, BufReader, Read};
5use std::num::ParseIntError;
6use std::sync::LazyLock;
7
8use chrono::{DateTime, FixedOffset, Utc};
9use regex::Regex;
10#[cfg(feature = "with_serde")]
11use serde::{Serialize, Serializer};
12use uuid::Uuid;
13
14#[allow(clippy::unwrap_used)]
15static KEY_VALUE_RE: LazyLock<Regex> = LazyLock::new(|| {
16 Regex::new(
17 r#"(?x)
18 ^\s*(.*?)\s*:\s*(.*?)\s*$
19 "#,
20 )
21 .unwrap()
22});
23
24#[allow(clippy::unwrap_used)]
25static THREAD_RE: LazyLock<Regex> = LazyLock::new(|| {
26 Regex::new(
27 r#"(?x)
28 ^Thread\ ([0-9]+)(\ Crashed)?:\s*(.+?)?\s*$
29 "#,
30 )
31 .unwrap()
32});
33
34#[allow(clippy::unwrap_used)]
35static THREAD_NAME_RE: LazyLock<Regex> = LazyLock::new(|| {
36 Regex::new(
37 r#"(?x)
38 ^Thread\ ([0-9]+)\ name:\s*(.+?)
39 (?:\s+Dispatch\ queue:\s*(.*?))?\s*$
40 "#,
41 )
42 .unwrap()
43});
44
45#[allow(clippy::unwrap_used)]
46static THREAD_STATE_RE: LazyLock<Regex> = LazyLock::new(|| {
47 Regex::new(
48 r#"(?x)
49 ^Thread\ ([0-9]+)\ crashed\ with\ .*?\ Thread\ State:\s*$
50 "#,
51 )
52 .unwrap()
53});
54
55#[allow(clippy::unwrap_used)]
56static REGISTER_RE: LazyLock<Regex> = LazyLock::new(|| {
57 Regex::new(
58 r#"(?x)
59 \s*
60 ([a-z0-9]+):\s+
61 (0x[0-9a-fA-F]+)\s*
62 "#,
63 )
64 .unwrap()
65});
66
67#[allow(clippy::unwrap_used)]
68static FRAME_RE: LazyLock<Regex> = LazyLock::new(|| {
69 Regex::new(
70 r#"(?x)
71 ^
72 [0-9]+ \s+
73 (.+?) \s+
74 (0x[0-9a-fA-F]+)\s+
75 (.*?)
76 (?:\ (?:\+\ [0-9]+|\((.*?):([0-9]+)\)))?
77 \s*
78 $
79 "#,
80 )
81 .unwrap()
82});
83
84#[allow(clippy::unwrap_used)]
85static BINARY_IMAGE_RE: LazyLock<Regex> = LazyLock::new(|| {
86 Regex::new(
87 r#"(?x)
88 ^
89 \s*
90 (0x[0-9a-fA-F]+) \s*
91 -
92 \s*
93 (0x[0-9a-fA-F]+) \s+
94 \+?(.+)\s+
95 (\S+?)\s+
96 (?:\(([^)]+?)\))?\s+
97 <([^>]+?)>\s+
98 (.*?)
99 $
100 "#,
101 )
102 .unwrap()
103});
104
105#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
107pub struct Addr(pub u64);
108
109#[cfg(feature = "with_serde")]
110impl Serialize for Addr {
111 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
112 where
113 S: Serializer,
114 {
115 format!("{:#x}", self.0).serialize(serializer)
116 }
117}
118
119#[derive(Debug, Default)]
121#[cfg_attr(feature = "with_serde", derive(Serialize))]
122pub struct AppleCrashReport {
123 pub incident_identifier: Uuid,
125 #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
127 pub timestamp: Option<DateTime<Utc>>,
128 #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
130 pub code_type: Option<String>,
131 #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
133 pub path: Option<String>,
134 #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
136 pub application_specific_information: Option<String>,
137 #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
139 pub filtered_syslog: Option<String>,
140 pub report_version: u32,
142 pub metadata: BTreeMap<String, String>,
144 pub threads: Vec<Thread>,
146 pub binary_images: Vec<BinaryImage>,
148}
149
150#[derive(Debug)]
152#[cfg_attr(feature = "with_serde", derive(Serialize))]
153pub struct BinaryImage {
154 pub addr: Addr,
156 pub size: u64,
158 pub uuid: Uuid,
160 pub arch: String,
162 #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
164 pub version: Option<String>,
165 pub name: String,
167 pub path: String,
169}
170
171#[derive(Debug)]
173#[cfg_attr(feature = "with_serde", derive(Serialize))]
174pub struct Frame {
175 #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
177 pub module: Option<String>,
178 #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
180 pub symbol: Option<String>,
181 #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
183 pub filename: Option<String>,
184 #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
186 pub lineno: Option<u32>,
187 pub instruction_addr: Addr,
189}
190
191#[derive(Debug)]
193#[cfg_attr(feature = "with_serde", derive(Serialize))]
194pub struct Thread {
195 pub id: u64,
197 #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
199 pub name: Option<String>,
200 #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
202 pub dispatch_queue: Option<String>,
203 pub crashed: bool,
205 pub frames: Vec<Frame>,
207 #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
209 pub registers: Option<BTreeMap<String, Addr>>,
210}
211
212enum ParsingState {
213 Root,
214 Thread,
215 BinaryImages,
216 ThreadState,
217 FilteredSyslog,
218 ApplicationSpecificInformation,
219}
220
221#[derive(Debug)]
222enum ParseErrorRepr {
223 Io(io::Error),
224 InvalidIncidentIdentifier(uuid::Error),
225 InvalidImageIdentifier(uuid::Error),
226 InvalidThreadId(ParseIntError),
227 InvalidRegisterAddress(ParseIntError),
228 InvalidFrameLineNumber(ParseIntError),
229 InvalidFrameInstructionAddress(ParseIntError),
230 InvalidBinaryImageAddress(ParseIntError),
231 InvalidBinaryImageEndAddress(ParseIntError),
232 InvalidBinaryImageAddressRange,
233 InvalidThreadState,
234 InvalidReportVersion(ParseIntError),
235 InvalidTimestamp(chrono::ParseError),
236}
237
238#[derive(Debug)]
240pub struct ParseError {
241 inner: ParseErrorRepr,
242}
243
244impl std::error::Error for ParseError {
245 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
246 match self.inner {
247 ParseErrorRepr::Io(ref err) => Some(err),
248 ParseErrorRepr::InvalidIncidentIdentifier(ref err) => Some(err),
249 ParseErrorRepr::InvalidImageIdentifier(ref err) => Some(err),
250 ParseErrorRepr::InvalidThreadId(ref err) => Some(err),
251 ParseErrorRepr::InvalidRegisterAddress(ref err) => Some(err),
252 ParseErrorRepr::InvalidFrameLineNumber(ref err) => Some(err),
253 ParseErrorRepr::InvalidFrameInstructionAddress(ref err) => Some(err),
254 ParseErrorRepr::InvalidBinaryImageAddress(ref err) => Some(err),
255 ParseErrorRepr::InvalidBinaryImageEndAddress(ref err) => Some(err),
256 ParseErrorRepr::InvalidBinaryImageAddressRange => None,
257 ParseErrorRepr::InvalidThreadState => None,
258 ParseErrorRepr::InvalidReportVersion(ref err) => Some(err),
259 ParseErrorRepr::InvalidTimestamp(ref err) => Some(err),
260 }
261 }
262}
263
264impl fmt::Display for ParseError {
265 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
266 match self.inner {
267 ParseErrorRepr::Io(..) => write!(f, "io error during parsing"),
268 ParseErrorRepr::InvalidIncidentIdentifier(..) => {
269 write!(f, "invalid incident identifier")
270 }
271 ParseErrorRepr::InvalidImageIdentifier(..) => {
272 write!(f, "invalid binary image identifier")
273 }
274 ParseErrorRepr::InvalidThreadId(..) => write!(f, "invalid thread identifier"),
275 ParseErrorRepr::InvalidRegisterAddress(..) => write!(f, "invalid register address"),
276 ParseErrorRepr::InvalidFrameLineNumber(..) => write!(f, "invalid frame line number"),
277 ParseErrorRepr::InvalidFrameInstructionAddress(..) => {
278 write!(f, "invalid frame instruction address")
279 }
280 ParseErrorRepr::InvalidBinaryImageAddress(..) => {
281 write!(f, "invalid binary image address")
282 }
283 ParseErrorRepr::InvalidBinaryImageEndAddress(..) => {
284 write!(f, "invalid binary image end address")
285 }
286 ParseErrorRepr::InvalidBinaryImageAddressRange => {
287 write!(f, "invalid binary image address range")
288 }
289 ParseErrorRepr::InvalidThreadState => write!(f, "invalid thread parser state"),
290 ParseErrorRepr::InvalidReportVersion(..) => write!(f, "invalid report version"),
291 ParseErrorRepr::InvalidTimestamp(..) => write!(f, "invalid timestamp"),
292 }
293 }
294}
295
296impl From<ParseErrorRepr> for ParseError {
297 fn from(value: ParseErrorRepr) -> Self {
298 Self { inner: value }
299 }
300}
301
302fn parse_prefixed_hex_u64(
303 value: &str,
304 err: fn(ParseIntError) -> ParseErrorRepr,
305) -> Result<u64, ParseErrorRepr> {
306 let value = value.strip_prefix("0x").unwrap_or(value);
307 u64::from_str_radix(value, 16).map_err(err)
308}
309
310impl std::str::FromStr for AppleCrashReport {
311 type Err = ParseError;
312
313 fn from_str(s: &str) -> Result<AppleCrashReport, ParseError> {
314 AppleCrashReport::from_line_iter(s.lines().map(|x| Ok(Cow::Borrowed(x))))
315 }
316}
317
318impl AppleCrashReport {
319 pub fn from_reader<R: Read>(r: R) -> Result<AppleCrashReport, ParseError> {
321 let reader = BufReader::new(r);
322 AppleCrashReport::from_line_iter(reader.lines().map(|x| x.map(Cow::Owned)))
323 }
324
325 #[allow(clippy::cognitive_complexity)]
326 fn from_line_iter<'a, I>(iter: I) -> Result<AppleCrashReport, ParseError>
327 where
328 I: Iterator<Item = Result<Cow<'a, str>, io::Error>>,
329 {
330 let mut state = ParsingState::Root;
331 let mut thread = None;
332 let mut thread_names = BTreeMap::new();
333 let mut registers = BTreeMap::new();
334 let mut application_specific_information = String::new();
335 let mut filtered_syslog = String::new();
336
337 let mut rv = AppleCrashReport::default();
338
339 for line in iter {
340 let line = line.map_err(ParseErrorRepr::Io)?;
341 let line = line.trim_end();
342
343 if line.starts_with("Binary Images:") {
344 state = ParsingState::BinaryImages;
345 continue;
346 } else if line.starts_with("Application Specific Information:") {
347 state = ParsingState::ApplicationSpecificInformation;
348 continue;
349 } else if line.starts_with("Filtered syslog:") {
350 state = ParsingState::FilteredSyslog;
351 continue;
352 } else if THREAD_STATE_RE.is_match(line) {
353 state = ParsingState::ThreadState;
354 continue;
355 } else if let Some(caps) = THREAD_RE.captures(line) {
356 if let Some(thread) = thread.take() {
357 rv.threads.push(thread);
358 }
359 thread = Some(Thread {
360 id: caps[1].parse().map_err(ParseErrorRepr::InvalidThreadId)?,
361 name: caps.get(3).map(|m| m.as_str().to_string()),
362 dispatch_queue: None,
363 frames: vec![],
364 crashed: caps.get(2).is_some(),
365 registers: None,
366 });
367 state = ParsingState::Thread;
368 continue;
369 } else if let Some(caps) = THREAD_NAME_RE.captures(line) {
370 thread_names.insert(
371 caps[1]
372 .parse::<u64>()
373 .map_err(ParseErrorRepr::InvalidThreadId)?,
374 (
375 caps[2].to_string(),
376 caps.get(3).map(|x| x.as_str().to_string()),
377 ),
378 );
379 state = ParsingState::Root;
380 continue;
381 }
382
383 state = match state {
384 ParsingState::Root => {
385 if let Some(caps) = KEY_VALUE_RE.captures(line) {
386 match &caps[1] {
387 "Incident Identifier" => {
388 rv.incident_identifier = caps[2]
389 .parse()
390 .map_err(ParseErrorRepr::InvalidIncidentIdentifier)?;
391 }
392 "Report Version" => {
393 rv.report_version = caps[2]
394 .parse()
395 .map_err(ParseErrorRepr::InvalidReportVersion)?;
396 }
397 "Path" => {
398 rv.path = Some(caps[2].to_string());
399 }
400 "Code Type" => {
401 rv.code_type = Some(caps[2].to_string());
402 }
403 "Date/Time" => {
404 let timestamp = DateTime::<FixedOffset>::parse_from_str(
405 &caps[2],
406 "%Y-%m-%d %H:%M:%S%.f %z",
407 )
408 .map_err(ParseErrorRepr::InvalidTimestamp)?;
409 rv.timestamp = Some(timestamp.with_timezone(&Utc));
410 }
411 "Crashed Thread" => {}
412 _ => {
413 rv.metadata.insert(caps[1].to_string(), caps[2].to_string());
414 }
415 }
416 }
417 ParsingState::Root
418 }
419 ParsingState::ThreadState => {
420 if line.is_empty() {
421 ParsingState::Root
422 } else {
423 for caps in REGISTER_RE.captures_iter(line) {
424 registers.insert(
425 caps[1].to_string(),
426 Addr(parse_prefixed_hex_u64(
427 &caps[2],
428 ParseErrorRepr::InvalidRegisterAddress,
429 )?),
430 );
431 }
432 ParsingState::ThreadState
433 }
434 }
435 ParsingState::Thread => {
436 if let Some(caps) = FRAME_RE.captures(line) {
437 let current_thread =
438 thread.as_mut().ok_or(ParseErrorRepr::InvalidThreadState)?;
439 current_thread.frames.push(Frame {
440 module: if &caps[1] == "???" {
441 None
442 } else {
443 Some(caps[1].to_string())
444 },
445 symbol: caps.get(3).and_then(|x| {
446 if x.as_str().starts_with("0x")
447 && u64::from_str_radix(&x.as_str()[2..], 16).is_ok()
448 {
449 None
450 } else {
451 Some(x.as_str().to_string())
452 }
453 }),
454 filename: caps.get(4).map(|x| x.as_str().to_string()),
455 lineno: caps
456 .get(5)
457 .map(|x| {
458 x.as_str()
459 .parse()
460 .map_err(ParseErrorRepr::InvalidFrameLineNumber)
461 })
462 .transpose()?,
463 instruction_addr: Addr(parse_prefixed_hex_u64(
464 &caps[2],
465 ParseErrorRepr::InvalidFrameInstructionAddress,
466 )?),
467 });
468 ParsingState::Thread
469 } else {
470 ParsingState::Root
471 }
472 }
473 ParsingState::BinaryImages => {
474 if line.is_empty() {
475 ParsingState::BinaryImages
476 } else if let Some(caps) = BINARY_IMAGE_RE.captures(line) {
477 let addr = parse_prefixed_hex_u64(
478 &caps[1],
479 ParseErrorRepr::InvalidBinaryImageAddress,
480 )?;
481 let end_addr = parse_prefixed_hex_u64(
482 &caps[2],
483 ParseErrorRepr::InvalidBinaryImageEndAddress,
484 )?;
485 rv.binary_images.push(BinaryImage {
486 addr: Addr(addr),
487 size: end_addr
488 .checked_sub(addr)
489 .ok_or(ParseErrorRepr::InvalidBinaryImageAddressRange)?,
490 uuid: caps[6]
491 .parse()
492 .map_err(ParseErrorRepr::InvalidImageIdentifier)?,
493 arch: caps[4].to_string(),
494 version: caps.get(5).map(|x| x.as_str().to_string()),
495 name: caps[3].to_string(),
496 path: caps[7].to_string(),
497 });
498 ParsingState::BinaryImages
499 } else {
500 ParsingState::Root
501 }
502 }
503 ParsingState::ApplicationSpecificInformation => {
504 if !application_specific_information.is_empty() {
505 application_specific_information.push('\n');
506 }
507 application_specific_information.push_str(line);
508 ParsingState::ApplicationSpecificInformation
509 }
510 ParsingState::FilteredSyslog => {
511 if !filtered_syslog.is_empty() {
512 filtered_syslog.push('\n');
513 }
514 filtered_syslog.push_str(line);
515 ParsingState::FilteredSyslog
516 }
517 }
518 }
519
520 if let Some(thread) = thread.take() {
521 rv.threads.push(thread);
522 }
523
524 for thread in rv.threads.iter_mut() {
525 if let Some((name, dispatch_queue)) = thread_names.remove(&thread.id) {
526 thread.name = Some(name);
527 thread.dispatch_queue = dispatch_queue;
528 }
529 }
530
531 if !registers.is_empty() {
532 for thread in rv.threads.iter_mut() {
533 if thread.crashed {
534 thread.registers = Some(registers);
535 break;
536 }
537 }
538 }
539
540 if !application_specific_information.is_empty() {
541 if application_specific_information.ends_with('\n') {
542 application_specific_information
543 .truncate(application_specific_information.len() - 1);
544 }
545 rv.application_specific_information = Some(application_specific_information);
546 }
547 if !filtered_syslog.is_empty() {
548 if filtered_syslog.ends_with('\n') {
549 filtered_syslog.truncate(filtered_syslog.len() - 1);
550 }
551 rv.filtered_syslog = Some(filtered_syslog);
552 }
553
554 Ok(rv)
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 const OVERFLOW_DECIMAL: &str = "18446744073709551616";
563 const OVERFLOW_HEX: &str = "0x10000000000000000";
564 const VALID_UUID: &str = "00000000000000000000000000000000";
565
566 #[test]
567 fn test_invalid_thread_ids() {
568 let result = format!("Thread {OVERFLOW_DECIMAL}:").parse::<AppleCrashReport>();
569 let err = result.unwrap_err();
570 assert!(matches!(err.inner, ParseErrorRepr::InvalidThreadId(_)));
571
572 let result = format!("Thread {OVERFLOW_DECIMAL} name: main").parse::<AppleCrashReport>();
573 let err = result.unwrap_err();
574 assert!(matches!(err.inner, ParseErrorRepr::InvalidThreadId(_)));
575 }
576
577 #[test]
578 fn test_invalid_register_addresses_return_parse_errors() {
579 let err = format!("Thread 0 crashed with X86-64 Thread State:\n rip: {OVERFLOW_HEX}")
580 .parse::<AppleCrashReport>()
581 .unwrap_err();
582
583 assert!(matches!(
584 err.inner,
585 ParseErrorRepr::InvalidRegisterAddress(_)
586 ));
587 }
588
589 #[test]
590 fn test_invalid_frame_numbers() {
591 let result =
592 format!("Thread 0:\n0 App {OVERFLOW_HEX} symbol + 1").parse::<AppleCrashReport>();
593 let err = result.unwrap_err();
594 assert!(matches!(
595 err.inner,
596 ParseErrorRepr::InvalidFrameInstructionAddress(_)
597 ));
598
599 let result = "Thread 0:\n0 App 0x0000000000000001 symbol (file.rs:4294967296)"
600 .parse::<AppleCrashReport>();
601 let err = result.unwrap_err();
602 assert!(matches!(
603 err.inner,
604 ParseErrorRepr::InvalidFrameLineNumber(_)
605 ));
606 }
607
608 #[test]
609 fn test_invalid_binary_image_addresses() {
610 let err = format!(
611 "Binary Images:\n {OVERFLOW_HEX} - 0x2 App arm64 <{VALID_UUID}> /App",
612 )
613 .parse::<AppleCrashReport>()
614 .unwrap_err();
615 assert!(matches!(
616 err.inner,
617 ParseErrorRepr::InvalidBinaryImageAddress(_)
618 ));
619
620 let err = format!(
621 "Binary Images:\n 0x1 - {OVERFLOW_HEX} App arm64 <{VALID_UUID}> /App",
622 )
623 .parse::<AppleCrashReport>()
624 .unwrap_err();
625 assert!(matches!(
626 err.inner,
627 ParseErrorRepr::InvalidBinaryImageEndAddress(_)
628 ));
629
630 let err =
631 format!("Binary Images:\n 0x2 - 0x1 App arm64 <{VALID_UUID}> /App",)
632 .parse::<AppleCrashReport>()
633 .unwrap_err();
634 assert!(matches!(
635 err.inner,
636 ParseErrorRepr::InvalidBinaryImageAddressRange
637 ));
638 }
639}