use std::backtrace::{Backtrace, BacktraceStatus};
#[derive(Debug)]
pub struct StackTrace {
pub frames: Vec<StackTraceFrame>,
}
#[derive(Debug)]
pub struct StackTraceFrame {
pub func: String,
pub file: String,
pub line: u32,
}
impl StackTrace {
pub fn parse(backtrace: &Backtrace) -> Option<Self> {
if backtrace.status() != BacktraceStatus::Captured {
return None;
}
let debug = format!("{:?}", backtrace);
match Self::parse_debug_str(&debug) {
Some(mut stacktrace) => {
stacktrace.nomalize();
Some(stacktrace)
}
None => None,
}
}
pub fn parse_debug_str(debug: &str) -> Option<Self> {
const LEADING: &str = "Backtrace ";
let debug = debug.trim();
if !debug.starts_with(LEADING) {
return None;
}
let frames_part = &debug[LEADING.len()..].trim();
if !frames_part.starts_with('[') || !frames_part.ends_with(']') {
return None;
}
let content = &frames_part[1..frames_part.len() - 1];
let mut frames = Vec::new();
let mut brace_depth = 0;
let mut current_frame_start = 0;
let mut chars = content.char_indices().peekable();
while let Some((i, ch)) = chars.next() {
match ch {
'{' => {
if brace_depth == 0 {
current_frame_start = i;
}
brace_depth += 1;
}
'}' => {
brace_depth -= 1;
if brace_depth == 0 {
let frame_str = &content[current_frame_start..=i];
if let Some(frame) = parse_single_frame(frame_str) {
frames.push(frame);
}
}
}
_ => {}
}
}
if frames.is_empty() {
None
} else {
let stacktrace = StackTrace { frames };
Some(stacktrace)
}
}
fn nomalize(&mut self) {
loop {
if let Some(first) = self.frames.first() {
if first.func.starts_with("std::backtrace")
|| first.func.starts_with("backerror::located_error")
{
self.frames.remove(0);
} else {
break;
}
} else {
break;
}
}
}
}
fn parse_single_frame(frame_str: &str) -> Option<StackTraceFrame> {
let trimmed = frame_str.trim();
if !trimmed.starts_with('{') || !trimmed.ends_with('}') {
return None;
}
let inner = &trimmed[1..trimmed.len() - 1].trim();
let func;
let mut file = String::new();
let mut line = 0u32;
if let Some(func_match) = find_key_value(inner, r#"fn:"#) {
func = func_match;
} else {
return None;
}
if let Some(file_match) = find_key_value(inner, r#"file:"#) {
file = file_match;
}
if let Some(line_str) = find_key_value(inner, r#"line:"#) {
if let Ok(parsed_line) = line_str.parse::<u32>() {
line = parsed_line;
}
}
Some(StackTraceFrame { func, file, line })
}
fn find_key_value(input: &str, key: &str) -> Option<String> {
let key_pos = input.find(key)?;
let after_key = &input[key_pos + key.len()..];
let trimmed_after_key = after_key.trim_start();
if trimmed_after_key.starts_with('"') {
let start_idx = key_pos + key.len() + trimmed_after_key.len()
- trimmed_after_key.trim_start_matches('"').len();
let mut chars = trimmed_after_key[1..].char_indices();
let mut prev_char = ' ';
while let Some((idx, ch)) = chars.next() {
if ch == '"' && prev_char != '\\' {
let value = &input[start_idx + 1..start_idx + idx + 1];
return Some(value.to_string());
}
prev_char = ch;
}
None
} else {
let start_idx = key_pos + key.len();
let remaining = &input[start_idx..];
let trimmed_remaining = remaining.trim_start();
let mut chars = trimmed_remaining.char_indices();
let mut end_pos = trimmed_remaining.len();
while let Some((idx, ch)) = chars.next() {
if ch == ','
|| (ch == ' '
&& idx + 1 < trimmed_remaining.len()
&& trimmed_remaining.chars().nth(idx + 1) == Some('}'))
|| ch == '}'
{
end_pos = idx;
break;
}
}
let value = trimmed_remaining[..end_pos].trim();
if !value.is_empty() {
Some(value.to_string())
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::StackTrace;
#[test]
fn parse_backtrace() {
let backtrace = std::backtrace::Backtrace::force_capture();
let msg = format!("{:?}", backtrace);
let stack = StackTrace::parse_debug_str(&msg);
assert!(stack.is_some());
let stack = stack.unwrap();
println!("len {}", stack.frames.len());
println!("{}", backtrace);
println!("{:?}", stack);
}
}