use std::borrow::Cow;
use std::fmt::{Display, Formatter, Result as FmtResult};
#[derive(Clone, Debug, PartialEq)]
pub struct StackTrace<'s> {
pub(crate) exception: Option<Throwable<'s>>,
pub(crate) frames: Vec<StackFrame<'s>>,
pub(crate) cause: Option<Box<StackTrace<'s>>>,
}
impl<'s> StackTrace<'s> {
pub fn new(exception: Option<Throwable<'s>>, frames: Vec<StackFrame<'s>>) -> Self {
Self {
exception,
frames,
cause: None,
}
}
pub fn with_cause(
exception: Option<Throwable<'s>>,
frames: Vec<StackFrame<'s>>,
cause: StackTrace<'s>,
) -> Self {
Self {
exception,
frames,
cause: Some(Box::new(cause)),
}
}
pub fn try_parse(stacktrace: &'s [u8]) -> Option<Self> {
let stacktrace = std::str::from_utf8(stacktrace).ok()?;
parse_stacktrace(stacktrace)
}
pub fn exception(&self) -> Option<&Throwable<'_>> {
self.exception.as_ref()
}
pub fn frames(&self) -> &[StackFrame<'_>] {
&self.frames
}
pub fn cause(&self) -> Option<&StackTrace<'_>> {
self.cause.as_deref()
}
}
impl Display for StackTrace<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
if let Some(exception) = &self.exception {
writeln!(f, "{exception}")?;
}
for frame in &self.frames {
writeln!(f, " {frame}")?;
}
if let Some(cause) = &self.cause {
write!(f, "Caused by: {cause}")?;
}
Ok(())
}
}
fn parse_stacktrace(content: &str) -> Option<StackTrace<'_>> {
let mut lines = content.lines().peekable();
let exception = lines.peek().and_then(|line| parse_throwable(line));
if exception.is_some() {
lines.next();
}
let mut stacktrace = StackTrace {
exception,
frames: vec![],
cause: None,
};
let mut current = &mut stacktrace;
for line in &mut lines {
if let Some(frame) = parse_frame(line) {
current.frames.push(frame);
} else if let Some(line) = line.strip_prefix("Caused by: ") {
current = current.cause.insert(Box::new(StackTrace {
exception: parse_throwable(line),
frames: vec![],
cause: None,
}));
}
}
if stacktrace.exception.is_some() || !stacktrace.frames.is_empty() {
Some(stacktrace)
} else {
None
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct StackFrame<'s> {
pub(crate) class: &'s str,
pub(crate) method: &'s str,
pub(crate) line: Option<usize>,
pub(crate) file: Option<Cow<'s, str>>,
pub(crate) parameters: Option<&'s str>,
pub(crate) method_synthesized: bool,
}
impl<'s> StackFrame<'s> {
pub fn new(class: &'s str, method: &'s str, line: usize) -> Self {
Self {
class,
method,
line: Some(line),
file: None,
parameters: None,
method_synthesized: false,
}
}
pub fn with_file(class: &'s str, method: &'s str, line: usize, file: &'s str) -> Self {
Self {
class,
method,
line: Some(line),
file: Some(Cow::Borrowed(file)),
parameters: None,
method_synthesized: false,
}
}
pub fn with_parameters(class: &'s str, method: &'s str, arguments: &'s str) -> Self {
Self {
class,
method,
line: None,
file: None,
parameters: Some(arguments),
method_synthesized: false,
}
}
pub fn with_method_synthesized(mut self, is_synthesized: bool) -> Self {
self.method_synthesized = is_synthesized;
self
}
pub fn try_parse(line: &'s [u8]) -> Option<Self> {
let line = std::str::from_utf8(line).ok()?;
parse_frame(line)
}
pub fn class(&self) -> &str {
self.class
}
pub fn method(&self) -> &str {
self.method
}
pub fn full_method(&self) -> String {
format!("{}.{}", self.class, self.method)
}
pub fn file(&self) -> Option<&str> {
self.file.as_deref()
}
pub fn line(&self) -> Option<usize> {
self.line
}
pub fn parameters(&self) -> Option<&str> {
self.parameters
}
pub fn method_synthesized(&self) -> bool {
self.method_synthesized
}
}
impl Display for StackFrame<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let file_name = self.file.as_deref().unwrap_or("<unknown>");
match self.line {
Some(line) => write!(
f,
"at {}.{}({}:{})",
self.class, self.method, file_name, line
),
None => write!(f, "at {}.{}({})", self.class, self.method, file_name),
}
}
}
pub(crate) fn parse_frame(line: &str) -> Option<StackFrame<'_>> {
let line = line.trim();
if !line.starts_with("at ") || !line.ends_with(')') {
return None;
}
let (method_split, file_split) = line[3..line.len() - 1].split_once('(')?;
let (class, method) = method_split.rsplit_once('.')?;
let (file, line) = match file_split.rsplit_once(':') {
Some((file, line)) => (file, Some(line.parse().unwrap_or(0))),
None => (file_split, None),
};
Some(StackFrame {
class,
method,
file: Some(Cow::Borrowed(file)),
line,
parameters: None,
method_synthesized: false,
})
}
#[derive(Clone, Debug, PartialEq)]
pub struct Throwable<'s> {
pub(crate) class: &'s str,
pub(crate) message: Option<&'s str>,
}
impl<'s> Throwable<'s> {
pub fn new(class: &'s str) -> Self {
Self {
class,
message: None,
}
}
pub fn with_message(class: &'s str, message: &'s str) -> Self {
Self {
class,
message: Some(message),
}
}
pub fn try_parse(line: &'s [u8]) -> Option<Self> {
std::str::from_utf8(line).ok().and_then(parse_throwable)
}
pub fn class(&self) -> &str {
self.class
}
pub fn message(&self) -> Option<&str> {
self.message
}
}
impl Display for Throwable<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}", self.class)?;
if let Some(message) = self.message {
write!(f, ": {message}")?;
}
Ok(())
}
}
pub(crate) fn parse_throwable(line: &str) -> Option<Throwable<'_>> {
let line = line.trim();
let mut class_split = line.splitn(2, ": ");
let class = class_split.next()?;
let message = class_split.next();
if class.contains(' ') {
None
} else {
Some(Throwable { class, message })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn print_stack_trace() {
let trace = StackTrace {
exception: Some(Throwable {
class: "com.example.MainFragment",
message: Some("Crash"),
}),
frames: vec![StackFrame {
class: "com.example.Util",
method: "show",
line: Some(5),
file: Some(Cow::Borrowed("Util.java")),
parameters: None,
method_synthesized: false,
}],
cause: Some(Box::new(StackTrace {
exception: Some(Throwable {
class: "com.example.Other",
message: Some("Invalid data"),
}),
frames: vec![StackFrame {
class: "com.example.Parser",
method: "parse",
line: Some(115),
file: None,
parameters: None,
method_synthesized: false,
}],
cause: None,
})),
};
let expect = "\
com.example.MainFragment: Crash
at com.example.Util.show(Util.java:5)
Caused by: com.example.Other: Invalid data
at com.example.Parser.parse(<unknown>:115)\n";
assert_eq!(expect, trace.to_string());
}
#[test]
fn stack_frame() {
let line = "at com.example.MainFragment.onClick(SourceFile:1)";
let stack_frame = parse_frame(line);
let expect = Some(StackFrame {
class: "com.example.MainFragment",
method: "onClick",
line: Some(1),
file: Some(Cow::Borrowed("SourceFile")),
parameters: None,
method_synthesized: false,
});
assert_eq!(expect, stack_frame);
let line = " at com.example.MainFragment.onClick(SourceFile:1)";
let stack_frame = parse_frame(line);
assert_eq!(expect, stack_frame);
let line = "\tat com.example.MainFragment.onClick(SourceFile:1)";
let stack_frame = parse_frame(line);
assert_eq!(expect, stack_frame);
}
#[test]
fn print_stack_frame() {
let frame = StackFrame {
class: "com.example.MainFragment",
method: "onClick",
line: Some(1),
file: None,
parameters: None,
method_synthesized: false,
};
assert_eq!(
"at com.example.MainFragment.onClick(<unknown>:1)",
frame.to_string()
);
let frame = StackFrame {
class: "com.example.MainFragment",
method: "onClick",
line: Some(1),
file: Some(Cow::Borrowed("SourceFile")),
parameters: None,
method_synthesized: false,
};
assert_eq!(
"at com.example.MainFragment.onClick(SourceFile:1)",
frame.to_string()
);
}
#[test]
fn throwable() {
let line = "com.example.MainFragment: Crash!";
let throwable = parse_throwable(line);
let expect = Some(Throwable {
class: "com.example.MainFragment",
message: Some("Crash!"),
});
assert_eq!(expect, throwable);
}
#[test]
fn print_throwable() {
let throwable = Throwable {
class: "com.example.MainFragment",
message: None,
};
assert_eq!("com.example.MainFragment", throwable.to_string());
let throwable = Throwable {
class: "com.example.MainFragment",
message: Some("Crash"),
};
assert_eq!("com.example.MainFragment: Crash", throwable.to_string());
}
}