use super::Error;
const INITIAL_SKIP_PREFIXES: &[&str] = &["std::backtrace", "human_errors"];
const HIDE_PATH_PREFIXES: &[&str] = &["core::", "std::"];
const RUNTIME_BOUNDARY_MARKER: &str = "std::sys::backtrace::__rust_begin_short_backtrace";
struct BacktraceFrame<'a> {
index: usize,
symbol: &'a str,
location: Option<&'a str>,
}
pub(crate) fn collect(error: &Error) -> Vec<(String, String)> {
let mut backtraces = Vec::new();
if let Some(backtrace) = captured(error) {
backtraces.push((error.description(), simplify(backtrace)));
}
let mut cause: Option<&(dyn std::error::Error + 'static)> = Some(error.error.as_ref());
while let Some(err) = cause {
if let Some(err) = err.downcast_ref::<Error>() {
if let Some(backtrace) = captured(err) {
backtraces.push((err.description(), simplify(backtrace)));
}
}
cause = err.source();
}
backtraces
}
fn captured(error: &Error) -> Option<&std::backtrace::Backtrace> {
error
.backtrace
.as_ref()
.filter(|backtrace| backtrace.status() == std::backtrace::BacktraceStatus::Captured)
}
fn simplify(backtrace: &std::backtrace::Backtrace) -> String {
simplify_text(&backtrace.to_string())
}
fn simplify_text(raw: &str) -> String {
let frames = parse_frames(raw);
if frames.is_empty() {
return raw.to_string();
}
let end = frames
.iter()
.position(|frame| frame.symbol.contains(RUNTIME_BOUNDARY_MARKER))
.unwrap_or(frames.len());
let bottom_skipped = frames.len() - end;
let start = frames[..end]
.iter()
.position(|frame| !starts_with_any(frame.symbol, INITIAL_SKIP_PREFIXES))
.unwrap_or(end);
let top_skipped = start;
let frames = &frames[start..end];
if frames.is_empty() {
return raw.to_string();
}
let mut output = String::new();
if top_skipped > 0 {
output.push_str(&format!(" ... skipped {top_skipped} frames ...\n"));
}
for frame in frames {
output.push_str(&format!("{:>2}: {}\n", frame.index, frame.symbol));
if let Some(location) = frame.location {
if !starts_with_any(frame.symbol, HIDE_PATH_PREFIXES) {
output.push_str(&format!(" {location}\n"));
}
}
}
if bottom_skipped > 0 {
output.push_str(&format!(" ... skipped {bottom_skipped} frames ...\n"));
}
output
}
fn parse_frames(raw: &str) -> Vec<BacktraceFrame<'_>> {
let mut frames = Vec::new();
let mut lines = raw.lines().peekable();
while let Some(line) = lines.next() {
let Some((index, symbol)) = parse_frame(line.trim_start()) else {
continue;
};
let location = match lines.peek() {
Some(next) if next.trim_start().starts_with("at ") => {
Some(lines.next().unwrap().trim_start())
}
_ => None,
};
frames.push(BacktraceFrame {
index,
symbol,
location,
});
}
frames
}
fn parse_frame(line: &str) -> Option<(usize, &str)> {
let (index, symbol) = line.split_once(": ")?;
Some((index.parse().ok()?, symbol))
}
fn starts_with_any(symbol: &str, prefixes: &[&str]) -> bool {
prefixes.iter().any(|prefix| symbol.starts_with(prefix))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simplify_text() {
let raw = "\
0: std::backtrace_rs::backtrace::libunwind::trace
at /rustc/library/std/src/backtrace.rs:1
1: std::backtrace::Backtrace::create
at /rustc/library/std/src/backtrace.rs:2
2: human_errors::error::Error::new
at ./src/error.rs:57
3: human_errors::helpers::wrap_system
at ./src/helpers.rs:110
4: my_app::do_work
at ./src/main.rs:20
5: my_app::main
at ./src/main.rs:10
6: core::ops::function::FnOnce::call_once
at /rustc/library/core/src/ops/function.rs:250
7: std::rt::lang_start_internal
at /rustc/library/std/src/rt.rs:175
8: main";
let simplified = simplify_text(raw);
assert!(!simplified.contains("std::backtrace"));
assert!(!simplified.contains("human_errors::"));
assert!(simplified.contains("... skipped 4 frames ..."));
assert!(simplified.contains(" 4: my_app::do_work"));
assert!(simplified.contains("at ./src/main.rs:20"));
assert!(simplified.contains("core::ops::function::FnOnce::call_once"));
assert!(simplified.contains("std::rt::lang_start_internal"));
assert!(!simplified.contains("at /rustc/library/core/src/ops/function.rs:250"));
assert!(!simplified.contains("at /rustc/library/std/src/rt.rs:175"));
assert!(simplified.contains("main\n"));
}
#[test]
fn test_simplify_text_strips_runtime_frames() {
let raw = "\
0: human_errors::error::Error::new
at ./src/error.rs:57
1: my_app::main
at ./src/main.rs:10
2: core::ops::function::FnOnce::call_once
at /rustc/library/core/src/ops/function.rs:250
3: std::sys::backtrace::__rust_begin_short_backtrace
at /rustc/library/std/src/sys/backtrace.rs:154
4: std::rt::lang_start_internal
at /rustc/library/std/src/rt.rs:175
5: main
6: BaseThreadInitThunk
7: RtlUserThreadStart";
let simplified = simplify_text(raw);
assert!(simplified.contains(" 1: my_app::main"));
assert!(simplified.contains("core::ops::function::FnOnce::call_once"));
assert!(!simplified.contains("__rust_begin_short_backtrace"));
assert!(!simplified.contains("std::rt::lang_start_internal"));
assert!(!simplified.contains("BaseThreadInitThunk"));
assert!(!simplified.contains("RtlUserThreadStart"));
assert!(simplified.contains("... skipped 5 frames ..."));
}
#[test]
fn test_simplify_text_falls_back_when_empty() {
let raw = "not a recognisable backtrace";
assert_eq!(simplify_text(raw), raw);
}
}