Skip to main content

apple_crash_report_parser/
parser.rs

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/// A newtype for addresses.
106#[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/// Holds a parsed apple crash report.
120#[derive(Debug, Default)]
121#[cfg_attr(feature = "with_serde", derive(Serialize))]
122pub struct AppleCrashReport {
123    /// The unique crash ID.
124    pub incident_identifier: Uuid,
125    /// The timestamp of the crash.
126    #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
127    pub timestamp: Option<DateTime<Utc>>,
128    /// The architecture of the crash (might require further parsing)
129    #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
130    pub code_type: Option<String>,
131    /// The path to the application.
132    #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
133    pub path: Option<String>,
134    /// Optional application specific crash information as string.
135    #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
136    pub application_specific_information: Option<String>,
137    /// Optional syslog info
138    #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
139    pub filtered_syslog: Option<String>,
140    /// The internal report version.
141    pub report_version: u32,
142    /// Extra metdata.
143    pub metadata: BTreeMap<String, String>,
144    /// A list of threads.
145    pub threads: Vec<Thread>,
146    /// A list of referenced binary images.
147    pub binary_images: Vec<BinaryImage>,
148}
149
150/// A single binary image in the crash.
151#[derive(Debug)]
152#[cfg_attr(feature = "with_serde", derive(Serialize))]
153pub struct BinaryImage {
154    /// The address of the image,
155    pub addr: Addr,
156    /// The size of the image,
157    pub size: u64,
158    /// The unique ID of the image,
159    pub uuid: Uuid,
160    /// The architecture of the image,
161    pub arch: String,
162    /// The version of the image if available. This might require further parsing.
163    #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
164    pub version: Option<String>,
165    /// The short name of the image.
166    pub name: String,
167    /// The full path of the image.
168    pub path: String,
169}
170
171/// Represents a single frame.
172#[derive(Debug)]
173#[cfg_attr(feature = "with_serde", derive(Serialize))]
174pub struct Frame {
175    /// The module of the frame.
176    #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
177    pub module: Option<String>,
178    /// The symbol of the frame if available.
179    #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
180    pub symbol: Option<String>,
181    /// The filename of the frame if available.
182    #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
183    pub filename: Option<String>,
184    /// The line number of the frame if available.
185    #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
186    pub lineno: Option<u32>,
187    //// The instruction address of the frame.
188    pub instruction_addr: Addr,
189}
190
191/// A single thread in the crash.
192#[derive(Debug)]
193#[cfg_attr(feature = "with_serde", derive(Serialize))]
194pub struct Thread {
195    /// The ID (index) of the thread.
196    pub id: u64,
197    /// The name of the thread if available.
198    #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
199    pub name: Option<String>,
200    /// The name of the dispatch queue
201    #[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
202    pub dispatch_queue: Option<String>,
203    /// `true` if this thread crashed.
204    pub crashed: bool,
205    /// The list of frames
206    pub frames: Vec<Frame>,
207    /// A dump of all the registers of the thread if available.
208    #[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/// Represents a parsing error.
239#[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    /// Consumes a reader and parses it.
320    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}