use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt;
use std::io::{self, BufRead, BufReader, Read};
use std::num::ParseIntError;
use std::sync::LazyLock;
use chrono::{DateTime, FixedOffset, Utc};
use regex::Regex;
#[cfg(feature = "with_serde")]
use serde::{Serialize, Serializer};
use uuid::Uuid;
#[allow(clippy::unwrap_used)]
static KEY_VALUE_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r#"(?x)
^\s*(.*?)\s*:\s*(.*?)\s*$
"#,
)
.unwrap()
});
#[allow(clippy::unwrap_used)]
static THREAD_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r#"(?x)
^Thread\ ([0-9]+)(\ Crashed)?:\s*(.+?)?\s*$
"#,
)
.unwrap()
});
#[allow(clippy::unwrap_used)]
static THREAD_NAME_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r#"(?x)
^Thread\ ([0-9]+)\ name:\s*(.+?)
(?:\s+Dispatch\ queue:\s*(.*?))?\s*$
"#,
)
.unwrap()
});
#[allow(clippy::unwrap_used)]
static THREAD_STATE_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r#"(?x)
^Thread\ ([0-9]+)\ crashed\ with\ .*?\ Thread\ State:\s*$
"#,
)
.unwrap()
});
#[allow(clippy::unwrap_used)]
static REGISTER_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r#"(?x)
\s*
([a-z0-9]+):\s+
(0x[0-9a-fA-F]+)\s*
"#,
)
.unwrap()
});
#[allow(clippy::unwrap_used)]
static FRAME_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r#"(?x)
^
[0-9]+ \s+
(.+?) \s+
(0x[0-9a-fA-F]+)\s+
(.*?)
(?:\ (?:\+\ [0-9]+|\((.*?):([0-9]+)\)))?
\s*
$
"#,
)
.unwrap()
});
#[allow(clippy::unwrap_used)]
static BINARY_IMAGE_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r#"(?x)
^
\s*
(0x[0-9a-fA-F]+) \s*
-
\s*
(0x[0-9a-fA-F]+) \s+
\+?(.+)\s+
(\S+?)\s+
(?:\(([^)]+?)\))?\s+
<([^>]+?)>\s+
(.*?)
$
"#,
)
.unwrap()
});
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Addr(pub u64);
#[cfg(feature = "with_serde")]
impl Serialize for Addr {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
format!("{:#x}", self.0).serialize(serializer)
}
}
#[derive(Debug, Default)]
#[cfg_attr(feature = "with_serde", derive(Serialize))]
pub struct AppleCrashReport {
pub incident_identifier: Uuid,
#[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
pub timestamp: Option<DateTime<Utc>>,
#[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
pub code_type: Option<String>,
#[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
pub path: Option<String>,
#[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
pub application_specific_information: Option<String>,
#[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
pub filtered_syslog: Option<String>,
pub report_version: u32,
pub metadata: BTreeMap<String, String>,
pub threads: Vec<Thread>,
pub binary_images: Vec<BinaryImage>,
}
#[derive(Debug)]
#[cfg_attr(feature = "with_serde", derive(Serialize))]
pub struct BinaryImage {
pub addr: Addr,
pub size: u64,
pub uuid: Uuid,
pub arch: String,
#[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
pub version: Option<String>,
pub name: String,
pub path: String,
}
#[derive(Debug)]
#[cfg_attr(feature = "with_serde", derive(Serialize))]
pub struct Frame {
#[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
pub module: Option<String>,
#[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
pub symbol: Option<String>,
#[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
pub filename: Option<String>,
#[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
pub lineno: Option<u32>,
pub instruction_addr: Addr,
}
#[derive(Debug)]
#[cfg_attr(feature = "with_serde", derive(Serialize))]
pub struct Thread {
pub id: u64,
#[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
pub name: Option<String>,
#[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
pub dispatch_queue: Option<String>,
pub crashed: bool,
pub frames: Vec<Frame>,
#[cfg_attr(feature = "with_serde", serde(skip_serializing_if = "Option::is_none"))]
pub registers: Option<BTreeMap<String, Addr>>,
}
enum ParsingState {
Root,
Thread,
BinaryImages,
ThreadState,
FilteredSyslog,
ApplicationSpecificInformation,
}
#[derive(Debug)]
enum ParseErrorRepr {
Io(io::Error),
InvalidIncidentIdentifier(uuid::Error),
InvalidImageIdentifier(uuid::Error),
InvalidThreadId(ParseIntError),
InvalidRegisterAddress(ParseIntError),
InvalidFrameLineNumber(ParseIntError),
InvalidFrameInstructionAddress(ParseIntError),
InvalidBinaryImageAddress(ParseIntError),
InvalidBinaryImageEndAddress(ParseIntError),
InvalidBinaryImageAddressRange,
InvalidThreadState,
InvalidReportVersion(ParseIntError),
InvalidTimestamp(chrono::ParseError),
}
#[derive(Debug)]
pub struct ParseError {
inner: ParseErrorRepr,
}
impl std::error::Error for ParseError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self.inner {
ParseErrorRepr::Io(ref err) => Some(err),
ParseErrorRepr::InvalidIncidentIdentifier(ref err) => Some(err),
ParseErrorRepr::InvalidImageIdentifier(ref err) => Some(err),
ParseErrorRepr::InvalidThreadId(ref err) => Some(err),
ParseErrorRepr::InvalidRegisterAddress(ref err) => Some(err),
ParseErrorRepr::InvalidFrameLineNumber(ref err) => Some(err),
ParseErrorRepr::InvalidFrameInstructionAddress(ref err) => Some(err),
ParseErrorRepr::InvalidBinaryImageAddress(ref err) => Some(err),
ParseErrorRepr::InvalidBinaryImageEndAddress(ref err) => Some(err),
ParseErrorRepr::InvalidBinaryImageAddressRange => None,
ParseErrorRepr::InvalidThreadState => None,
ParseErrorRepr::InvalidReportVersion(ref err) => Some(err),
ParseErrorRepr::InvalidTimestamp(ref err) => Some(err),
}
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.inner {
ParseErrorRepr::Io(..) => write!(f, "io error during parsing"),
ParseErrorRepr::InvalidIncidentIdentifier(..) => {
write!(f, "invalid incident identifier")
}
ParseErrorRepr::InvalidImageIdentifier(..) => {
write!(f, "invalid binary image identifier")
}
ParseErrorRepr::InvalidThreadId(..) => write!(f, "invalid thread identifier"),
ParseErrorRepr::InvalidRegisterAddress(..) => write!(f, "invalid register address"),
ParseErrorRepr::InvalidFrameLineNumber(..) => write!(f, "invalid frame line number"),
ParseErrorRepr::InvalidFrameInstructionAddress(..) => {
write!(f, "invalid frame instruction address")
}
ParseErrorRepr::InvalidBinaryImageAddress(..) => {
write!(f, "invalid binary image address")
}
ParseErrorRepr::InvalidBinaryImageEndAddress(..) => {
write!(f, "invalid binary image end address")
}
ParseErrorRepr::InvalidBinaryImageAddressRange => {
write!(f, "invalid binary image address range")
}
ParseErrorRepr::InvalidThreadState => write!(f, "invalid thread parser state"),
ParseErrorRepr::InvalidReportVersion(..) => write!(f, "invalid report version"),
ParseErrorRepr::InvalidTimestamp(..) => write!(f, "invalid timestamp"),
}
}
}
impl From<ParseErrorRepr> for ParseError {
fn from(value: ParseErrorRepr) -> Self {
Self { inner: value }
}
}
fn parse_prefixed_hex_u64(
value: &str,
err: fn(ParseIntError) -> ParseErrorRepr,
) -> Result<u64, ParseErrorRepr> {
let value = value.strip_prefix("0x").unwrap_or(value);
u64::from_str_radix(value, 16).map_err(err)
}
impl std::str::FromStr for AppleCrashReport {
type Err = ParseError;
fn from_str(s: &str) -> Result<AppleCrashReport, ParseError> {
AppleCrashReport::from_line_iter(s.lines().map(|x| Ok(Cow::Borrowed(x))))
}
}
impl AppleCrashReport {
pub fn from_reader<R: Read>(r: R) -> Result<AppleCrashReport, ParseError> {
let reader = BufReader::new(r);
AppleCrashReport::from_line_iter(reader.lines().map(|x| x.map(Cow::Owned)))
}
#[allow(clippy::cognitive_complexity)]
fn from_line_iter<'a, I>(iter: I) -> Result<AppleCrashReport, ParseError>
where
I: Iterator<Item = Result<Cow<'a, str>, io::Error>>,
{
let mut state = ParsingState::Root;
let mut thread = None;
let mut thread_names = BTreeMap::new();
let mut registers = BTreeMap::new();
let mut application_specific_information = String::new();
let mut filtered_syslog = String::new();
let mut rv = AppleCrashReport::default();
for line in iter {
let line = line.map_err(ParseErrorRepr::Io)?;
let line = line.trim_end();
if line.starts_with("Binary Images:") {
state = ParsingState::BinaryImages;
continue;
} else if line.starts_with("Application Specific Information:") {
state = ParsingState::ApplicationSpecificInformation;
continue;
} else if line.starts_with("Filtered syslog:") {
state = ParsingState::FilteredSyslog;
continue;
} else if THREAD_STATE_RE.is_match(line) {
state = ParsingState::ThreadState;
continue;
} else if let Some(caps) = THREAD_RE.captures(line) {
if let Some(thread) = thread.take() {
rv.threads.push(thread);
}
thread = Some(Thread {
id: caps[1].parse().map_err(ParseErrorRepr::InvalidThreadId)?,
name: caps.get(3).map(|m| m.as_str().to_string()),
dispatch_queue: None,
frames: vec![],
crashed: caps.get(2).is_some(),
registers: None,
});
state = ParsingState::Thread;
continue;
} else if let Some(caps) = THREAD_NAME_RE.captures(line) {
thread_names.insert(
caps[1]
.parse::<u64>()
.map_err(ParseErrorRepr::InvalidThreadId)?,
(
caps[2].to_string(),
caps.get(3).map(|x| x.as_str().to_string()),
),
);
state = ParsingState::Root;
continue;
}
state = match state {
ParsingState::Root => {
if let Some(caps) = KEY_VALUE_RE.captures(line) {
match &caps[1] {
"Incident Identifier" => {
rv.incident_identifier = caps[2]
.parse()
.map_err(ParseErrorRepr::InvalidIncidentIdentifier)?;
}
"Report Version" => {
rv.report_version = caps[2]
.parse()
.map_err(ParseErrorRepr::InvalidReportVersion)?;
}
"Path" => {
rv.path = Some(caps[2].to_string());
}
"Code Type" => {
rv.code_type = Some(caps[2].to_string());
}
"Date/Time" => {
let timestamp = DateTime::<FixedOffset>::parse_from_str(
&caps[2],
"%Y-%m-%d %H:%M:%S%.f %z",
)
.map_err(ParseErrorRepr::InvalidTimestamp)?;
rv.timestamp = Some(timestamp.with_timezone(&Utc));
}
"Crashed Thread" => {}
_ => {
rv.metadata.insert(caps[1].to_string(), caps[2].to_string());
}
}
}
ParsingState::Root
}
ParsingState::ThreadState => {
if line.is_empty() {
ParsingState::Root
} else {
for caps in REGISTER_RE.captures_iter(line) {
registers.insert(
caps[1].to_string(),
Addr(parse_prefixed_hex_u64(
&caps[2],
ParseErrorRepr::InvalidRegisterAddress,
)?),
);
}
ParsingState::ThreadState
}
}
ParsingState::Thread => {
if let Some(caps) = FRAME_RE.captures(line) {
let current_thread =
thread.as_mut().ok_or(ParseErrorRepr::InvalidThreadState)?;
current_thread.frames.push(Frame {
module: if &caps[1] == "???" {
None
} else {
Some(caps[1].to_string())
},
symbol: caps.get(3).and_then(|x| {
if x.as_str().starts_with("0x")
&& u64::from_str_radix(&x.as_str()[2..], 16).is_ok()
{
None
} else {
Some(x.as_str().to_string())
}
}),
filename: caps.get(4).map(|x| x.as_str().to_string()),
lineno: caps
.get(5)
.map(|x| {
x.as_str()
.parse()
.map_err(ParseErrorRepr::InvalidFrameLineNumber)
})
.transpose()?,
instruction_addr: Addr(parse_prefixed_hex_u64(
&caps[2],
ParseErrorRepr::InvalidFrameInstructionAddress,
)?),
});
ParsingState::Thread
} else {
ParsingState::Root
}
}
ParsingState::BinaryImages => {
if line.is_empty() {
ParsingState::BinaryImages
} else if let Some(caps) = BINARY_IMAGE_RE.captures(line) {
let addr = parse_prefixed_hex_u64(
&caps[1],
ParseErrorRepr::InvalidBinaryImageAddress,
)?;
let end_addr = parse_prefixed_hex_u64(
&caps[2],
ParseErrorRepr::InvalidBinaryImageEndAddress,
)?;
rv.binary_images.push(BinaryImage {
addr: Addr(addr),
size: end_addr
.checked_sub(addr)
.ok_or(ParseErrorRepr::InvalidBinaryImageAddressRange)?,
uuid: caps[6]
.parse()
.map_err(ParseErrorRepr::InvalidImageIdentifier)?,
arch: caps[4].to_string(),
version: caps.get(5).map(|x| x.as_str().to_string()),
name: caps[3].to_string(),
path: caps[7].to_string(),
});
ParsingState::BinaryImages
} else {
ParsingState::Root
}
}
ParsingState::ApplicationSpecificInformation => {
if !application_specific_information.is_empty() {
application_specific_information.push('\n');
}
application_specific_information.push_str(line);
ParsingState::ApplicationSpecificInformation
}
ParsingState::FilteredSyslog => {
if !filtered_syslog.is_empty() {
filtered_syslog.push('\n');
}
filtered_syslog.push_str(line);
ParsingState::FilteredSyslog
}
}
}
if let Some(thread) = thread.take() {
rv.threads.push(thread);
}
for thread in rv.threads.iter_mut() {
if let Some((name, dispatch_queue)) = thread_names.remove(&thread.id) {
thread.name = Some(name);
thread.dispatch_queue = dispatch_queue;
}
}
if !registers.is_empty() {
for thread in rv.threads.iter_mut() {
if thread.crashed {
thread.registers = Some(registers);
break;
}
}
}
if !application_specific_information.is_empty() {
if application_specific_information.ends_with('\n') {
application_specific_information
.truncate(application_specific_information.len() - 1);
}
rv.application_specific_information = Some(application_specific_information);
}
if !filtered_syslog.is_empty() {
if filtered_syslog.ends_with('\n') {
filtered_syslog.truncate(filtered_syslog.len() - 1);
}
rv.filtered_syslog = Some(filtered_syslog);
}
Ok(rv)
}
}
#[cfg(test)]
mod tests {
use super::*;
const OVERFLOW_DECIMAL: &str = "18446744073709551616";
const OVERFLOW_HEX: &str = "0x10000000000000000";
const VALID_UUID: &str = "00000000000000000000000000000000";
#[test]
fn test_invalid_thread_ids() {
let result = format!("Thread {OVERFLOW_DECIMAL}:").parse::<AppleCrashReport>();
let err = result.unwrap_err();
assert!(matches!(err.inner, ParseErrorRepr::InvalidThreadId(_)));
let result = format!("Thread {OVERFLOW_DECIMAL} name: main").parse::<AppleCrashReport>();
let err = result.unwrap_err();
assert!(matches!(err.inner, ParseErrorRepr::InvalidThreadId(_)));
}
#[test]
fn test_invalid_register_addresses_return_parse_errors() {
let err = format!("Thread 0 crashed with X86-64 Thread State:\n rip: {OVERFLOW_HEX}")
.parse::<AppleCrashReport>()
.unwrap_err();
assert!(matches!(
err.inner,
ParseErrorRepr::InvalidRegisterAddress(_)
));
}
#[test]
fn test_invalid_frame_numbers() {
let result =
format!("Thread 0:\n0 App {OVERFLOW_HEX} symbol + 1").parse::<AppleCrashReport>();
let err = result.unwrap_err();
assert!(matches!(
err.inner,
ParseErrorRepr::InvalidFrameInstructionAddress(_)
));
let result = "Thread 0:\n0 App 0x0000000000000001 symbol (file.rs:4294967296)"
.parse::<AppleCrashReport>();
let err = result.unwrap_err();
assert!(matches!(
err.inner,
ParseErrorRepr::InvalidFrameLineNumber(_)
));
}
#[test]
fn test_invalid_binary_image_addresses() {
let err = format!(
"Binary Images:\n {OVERFLOW_HEX} - 0x2 App arm64 <{VALID_UUID}> /App",
)
.parse::<AppleCrashReport>()
.unwrap_err();
assert!(matches!(
err.inner,
ParseErrorRepr::InvalidBinaryImageAddress(_)
));
let err = format!(
"Binary Images:\n 0x1 - {OVERFLOW_HEX} App arm64 <{VALID_UUID}> /App",
)
.parse::<AppleCrashReport>()
.unwrap_err();
assert!(matches!(
err.inner,
ParseErrorRepr::InvalidBinaryImageEndAddress(_)
));
let err =
format!("Binary Images:\n 0x2 - 0x1 App arm64 <{VALID_UUID}> /App",)
.parse::<AppleCrashReport>()
.unwrap_err();
assert!(matches!(
err.inner,
ParseErrorRepr::InvalidBinaryImageAddressRange
));
}
}