Skip to main content

rootcause_backtrace/
lib.rs

1#![deny(
2    elided_lifetimes_in_paths,
3    missing_docs,
4    unsafe_code,
5    rustdoc::invalid_rust_codeblocks,
6    rustdoc::broken_intra_doc_links,
7    missing_copy_implementations,
8    unused_doc_comments
9)]
10
11//! Stack backtrace attachment collector for rootcause error reports.
12//!
13//! This crate provides functionality to automatically capture and attach stack
14//! backtraces to error reports. This is useful for debugging to see the call
15//! stack that led to an error.
16//!
17//! # Quick Start
18//!
19//! ## Using Hooks (Automatic for All Errors)
20//!
21//! Register a backtrace collector as a hook to automatically capture backtraces
22//! for all errors:
23//!
24//! ```
25//! use rootcause::hooks::Hooks;
26//! use rootcause_backtrace::BacktraceCollector;
27//!
28//! // Capture backtraces for all errors
29//! Hooks::new()
30//!     .report_creation_hook(BacktraceCollector::new_from_env())
31//!     .install()
32//!     .expect("failed to install hooks");
33//!
34//! // Now all errors automatically get backtraces!
35//! fn example() -> rootcause::Report {
36//!     rootcause::report!("something went wrong")
37//! }
38//! println!("{}", example().context("additional context"));
39//! ```
40//!
41//! This will print a backtrace similar to the following:
42//! ```text
43//!  ● additional context
44//!  ├ src/main.rs:12
45//!  ├ Backtrace
46//!  │ │ main - /build/src/main.rs:12
47//!  │ │ note: 39 frame(s) omitted. For a complete backtrace, set RUST_BACKTRACE=full.
48//!  │ ╰─
49//!  │
50//!  ● something went wrong
51//!  ├ src/main.rs:10
52//!  ╰ Backtrace
53//!    │ example - /build/src/main.rs:10
54//!    │ main    - /build/src/main.rs:12
55//!    │ note: 40 frame(s) omitted. For a complete backtrace, set RUST_BACKTRACE=full.
56//!    ╰─
57//! ```
58//!
59//! ## Manual Attachment (Per-Error)
60//!
61//! Attach backtraces to specific errors using the extension trait:
62//!
63//! ```
64//! use std::io;
65//!
66//! use rootcause::{Report, report};
67//! use rootcause_backtrace::BacktraceExt;
68//!
69//! fn operation() -> Result<(), Report> {
70//!     Err(report!("operation failed"))
71//! }
72//!
73//! // Attach backtrace to the error in the Result
74//! let result = operation().attach_backtrace();
75//! ```
76//!
77//! # Environment Variables
78//!
79//! - `RUST_BACKTRACE=full` - Disables filtering and shows full paths
80//! - `ROOTCAUSE_BACKTRACE` - Comma-separated options:
81//!   - `leafs` - Only capture backtraces for leaf errors (errors without
82//!     children)
83//!   - `full_paths` - Show full file paths in backtraces
84//!
85//! # Path privacy
86//!
87//! By default, backtrace paths are shortened paths for improved readability,
88//! but this may still expose private file system structure when a path is not
89//! recognized as belonging to a known prefix (e.g., RUST_SRC).
90//!
91//! If exposing private file system paths is a concern, then we recommend using
92//! the `--remap-path-prefix` option of `rustc` to remap source paths to
93//! generic placeholders.
94//!
95//! A good default way to handle this is to set the following environment
96//! variables when building your application for release:
97//!
98//! ```sh
99//! export RUSTFLAGS="--remap-path-prefix=$HOME=/home/user --remap-path-prefix=$PWD=/build"
100//! ```
101//!
102//! # Debugging symbols in release builds
103//!
104//! To ensure that backtraces contain useful symbol and source location
105//! information in release builds, make sure to enable debug symbols in your
106//! `Cargo.toml`:
107//!
108//! ```toml
109//! [profile.release]
110//! strip = false
111//! # You can also set this to "line-tables-only" for smaller binaries
112//! debug = true
113//! ```
114//!
115//! # Filtering
116//!
117//! Control which frames appear in backtraces:
118//!
119//! ```
120//! use rootcause_backtrace::{BacktraceCollector, BacktraceFilter};
121//!
122//! let collector = BacktraceCollector {
123//!     filter: BacktraceFilter {
124//!         skipped_initial_crates: &["rootcause", "rootcause-backtrace"],  // Skip frames from rootcause at start
125//!         skipped_middle_crates: &["tokio"],     // Skip tokio frames in middle
126//!         skipped_final_crates: &["std"],        // Skip std frames at end
127//!         max_entry_count: 15,                   // Limit to 15 frames
128//!         show_full_path: false,                 // Show shortened paths
129//!     },
130//!     capture_backtrace_for_reports_with_children: false,  // Only leaf errors
131//! };
132//! ```
133
134use std::{borrow::Cow, fmt, panic::Location, sync::OnceLock};
135
136use backtrace::BytesOrWideString;
137use rootcause::{
138    Report, ReportMut,
139    handlers::{
140        AttachmentFormattingPlacement, AttachmentFormattingStyle, AttachmentHandler,
141        FormattingFunction,
142    },
143    hooks::report_creation::ReportCreationHook,
144    markers::{self, Dynamic, ObjectMarkerFor},
145    report_attachment::ReportAttachment,
146};
147
148/// Stack backtrace information.
149///
150/// Contains a collection of stack frames representing the call stack
151/// at the point where a report was created.
152///
153/// # Examples
154///
155/// Capture a backtrace manually:
156///
157/// ```
158/// use rootcause_backtrace::{Backtrace, BacktraceFilter};
159///
160/// let backtrace = Backtrace::capture(&BacktraceFilter::DEFAULT);
161/// if let Some(bt) = backtrace {
162///     println!("Captured {} frames", bt.entries.len());
163/// }
164/// ```
165#[derive(Debug, Clone)]
166pub struct Backtrace {
167    /// The entries in the backtrace, ordered from most recent to oldest.
168    pub entries: Vec<BacktraceEntry>,
169    /// Total number of frames that were omitted due to filtering.
170    pub total_omitted_frames: usize,
171}
172
173/// A single entry in a stack backtrace.
174#[derive(Debug, Clone)]
175pub enum BacktraceEntry {
176    /// A normal stack frame.
177    Frame(Frame),
178    /// A group of omitted frames from a specific crate.
179    OmittedFrames {
180        /// Number of omitted frames.
181        count: usize,
182        /// The name of the crate whose frames were omitted.
183        skipped_crate: &'static str,
184    },
185}
186
187/// A single stack frame in a backtrace.
188///
189/// Represents one function call in the call stack, including symbol information
190/// and source location if available.
191#[derive(Debug, Clone)]
192pub struct Frame {
193    /// The demangled symbol name for this frame.
194    pub sym_demangled: String,
195    /// File path information for this frame, if available.
196    pub frame_path: Option<FramePath>,
197    /// Line number in the source file, if available.
198    pub lineno: Option<u32>,
199}
200
201/// File path information for a stack frame.
202///
203/// Contains the raw path and processed components for better display
204/// formatting.
205#[derive(Debug, Clone)]
206pub struct FramePath {
207    /// The raw file path from the debug information.
208    pub raw_path: String,
209    /// The crate name if detected from the path.
210    pub crate_name: Option<Cow<'static, str>>,
211    /// Common path prefix information for shortening display.
212    pub split_path: Option<FramePrefix>,
213}
214
215/// A common prefix for a frame path.
216///
217/// This struct represents a decomposed file path where a known prefix
218/// has been identified and separated from the rest of the path.
219#[derive(Debug, Clone)]
220pub struct FramePrefix {
221    /// The kind of prefix used to identify this prefix.
222    ///
223    /// Examples: `"RUST_SRC"` for Rust standard library paths,
224    /// `"CARGO"` for Cargo registry crate paths,
225    /// `"ROOTCAUSE"` for rootcause library paths.
226    pub prefix_kind: &'static str,
227    /// The full prefix path that was removed from the original path.
228    ///
229    /// Example: `"/home/user/.cargo/registry/src/index.crates.
230    /// io-1949cf8c6b5b557f"`
231    pub prefix: String,
232    /// The remaining path after the prefix was removed.
233    ///
234    /// Example: `"indexmap-2.12.1/src/map/core/entry.rs"`
235    pub suffix: String,
236}
237
238/// Handler for formatting [`Backtrace`] attachments.
239#[derive(Copy, Clone)]
240pub struct BacktraceHandler<const SHOW_FULL_PATH: bool>;
241
242fn get_function_name(s: &str) -> &str {
243    let mut word_start = 0usize;
244    let mut word_end = 0usize;
245    let mut angle_nesting_level = 0u64;
246    let mut curly_nesting_level = 0u64;
247    let mut potential_function_arrow = false;
248    let mut inside_word = false;
249
250    for (i, c) in s.char_indices() {
251        if curly_nesting_level == 0 && angle_nesting_level == 0 {
252            if !inside_word && unicode_ident::is_xid_start(c) {
253                word_start = i;
254                inside_word = true;
255            } else if inside_word && !unicode_ident::is_xid_continue(c) {
256                word_end = i;
257                inside_word = false;
258            }
259        }
260
261        let was_potential_function_arrow = potential_function_arrow;
262        potential_function_arrow = c == '-';
263
264        if c == '<' {
265            angle_nesting_level = angle_nesting_level.saturating_add(1);
266        } else if c == '>' && !was_potential_function_arrow {
267            angle_nesting_level = angle_nesting_level.saturating_sub(1);
268        } else if c == '{' {
269            curly_nesting_level = curly_nesting_level.saturating_add(1);
270            if !inside_word && curly_nesting_level == 1 && angle_nesting_level == 0 {
271                word_start = i;
272                inside_word = true;
273            }
274        } else if c == '}' {
275            curly_nesting_level = curly_nesting_level.saturating_sub(1);
276            if inside_word && curly_nesting_level == 0 {
277                word_end = i + 1;
278                inside_word = false;
279            }
280        }
281    }
282
283    if word_start < word_end {
284        &s[word_start..word_end]
285    } else {
286        // We started at word start but never found an end; return rest of string
287        &s[word_start..]
288    }
289}
290
291impl<const SHOW_FULL_PATH: bool> AttachmentHandler<Backtrace> for BacktraceHandler<SHOW_FULL_PATH> {
292    fn display(value: &Backtrace, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293        const MAX_UNWRAPPED_SYM_LENGTH: usize = 25;
294        let mut max_seen_length = 0;
295        for entry in &value.entries {
296            if let BacktraceEntry::Frame(frame) = entry {
297                let sym = get_function_name(&frame.sym_demangled);
298                if sym.len() <= MAX_UNWRAPPED_SYM_LENGTH && sym.len() > max_seen_length {
299                    max_seen_length = sym.len();
300                }
301            }
302        }
303
304        for entry in &value.entries {
305            match entry {
306                BacktraceEntry::OmittedFrames {
307                    count,
308                    skipped_crate,
309                } => {
310                    writeln!(
311                        f,
312                        "... omitted {count} frame(s) from crate '{skipped_crate}' ..."
313                    )?;
314                    continue;
315                }
316                BacktraceEntry::Frame(frame) => {
317                    let sym = get_function_name(&frame.sym_demangled);
318
319                    if sym.len() <= MAX_UNWRAPPED_SYM_LENGTH {
320                        write!(f, "{:<max_seen_length$} - ", sym)?;
321                    } else {
322                        write!(f, "{sym}\n   - ")?;
323                    }
324
325                    if let Some(path) = &frame.frame_path {
326                        if SHOW_FULL_PATH {
327                            write!(f, "{}", path.raw_path)?;
328                        } else if let Some(split_path) = &path.split_path {
329                            write!(f, "[..]/{}", split_path.suffix)?;
330                        } else {
331                            write!(f, "{}", path.raw_path)?;
332                        }
333
334                        if let Some(lineno) = frame.lineno {
335                            write!(f, ":{lineno}")?;
336                        }
337                    }
338                    writeln!(f)?;
339                }
340            }
341        }
342
343        if value.total_omitted_frames > 0 {
344            writeln!(
345                f,
346                "note: {} frame(s) omitted. For a complete backtrace, set RUST_BACKTRACE=full.",
347                value.total_omitted_frames
348            )?;
349        }
350
351        Ok(())
352    }
353
354    fn debug(value: &Backtrace, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
355        std::fmt::Debug::fmt(value, formatter)
356    }
357
358    fn preferred_formatting_style(
359        backtrace: &Backtrace,
360        _report_formatting_function: FormattingFunction,
361    ) -> AttachmentFormattingStyle {
362        AttachmentFormattingStyle {
363            placement: if backtrace.entries.is_empty() {
364                AttachmentFormattingPlacement::Hidden
365            } else {
366                AttachmentFormattingPlacement::InlineWithHeader {
367                    header: "Backtrace",
368                }
369            },
370            // No reason to every print the Backtrace in the report
371            // as anything other than display.
372            function: FormattingFunction::Display,
373            priority: 10,
374        }
375    }
376}
377
378/// Attachment collector for capturing stack backtraces.
379///
380/// When registered as a report creation hook, this collector automatically
381/// captures the current stack backtrace and attaches it as a [`Backtrace`]
382/// attachment.
383///
384/// # Examples
385///
386/// Basic usage with default settings:
387///
388/// ```
389/// use rootcause::hooks::Hooks;
390/// use rootcause_backtrace::BacktraceCollector;
391///
392/// Hooks::new()
393///     .report_creation_hook(BacktraceCollector::new_from_env())
394///     .install()
395///     .expect("failed to install hooks");
396/// ```
397///
398/// Custom configuration:
399///
400/// ```
401/// use rootcause::hooks::Hooks;
402/// use rootcause_backtrace::{BacktraceCollector, BacktraceFilter};
403///
404/// let collector = BacktraceCollector {
405///     filter: BacktraceFilter {
406///         skipped_initial_crates: &[],
407///         skipped_middle_crates: &[],
408///         skipped_final_crates: &[],
409///         max_entry_count: 30,
410///         show_full_path: true,
411///     },
412///     capture_backtrace_for_reports_with_children: true,
413/// };
414///
415/// Hooks::new()
416///     .report_creation_hook(collector)
417///     .install()
418///     .expect("failed to install hooks");
419/// ```
420#[derive(Copy, Clone)]
421pub struct BacktraceCollector {
422    /// Configuration for filtering and formatting backtrace frames.
423    pub filter: BacktraceFilter,
424
425    /// If set to true, a backtrace is captured for every report creation,
426    /// including reports that have child reports (i.e., reports created with
427    /// existing children). If set to false, a backtrace is captured only
428    /// for reports created without any children. Reports created without
429    /// children always receive a backtrace regardless of this setting.
430    pub capture_backtrace_for_reports_with_children: bool,
431}
432
433/// Configuration for filtering frames from certain crates in a backtrace.
434///
435/// # Examples
436///
437/// Use default filtering:
438///
439/// ```
440/// use rootcause_backtrace::BacktraceFilter;
441///
442/// let filter = BacktraceFilter::DEFAULT;
443/// ```
444///
445/// Custom filtering to focus on application code:
446///
447/// ```
448/// use rootcause_backtrace::BacktraceFilter;
449///
450/// let filter = BacktraceFilter {
451///     // Hide rootcause crate frames at the start
452///     skipped_initial_crates: &["rootcause", "rootcause-backtrace"],
453///     // Hide framework frames in the middle
454///     skipped_middle_crates: &["tokio", "hyper", "tower"],
455///     // Hide runtime frames at the end
456///     skipped_final_crates: &["std", "tokio"],
457///     // Show only the most relevant 10 frames
458///     max_entry_count: 10,
459///     // Show shortened paths
460///     show_full_path: false,
461/// };
462/// ```
463#[derive(Copy, Clone, Debug)]
464pub struct BacktraceFilter {
465    /// Set of crate names whose frames should be hidden when they appear
466    /// at the beginning of a backtrace.
467    pub skipped_initial_crates: &'static [&'static str],
468    /// Set of crate names whose frames should be hidden when they appear
469    /// in the middle of a backtrace.
470    pub skipped_middle_crates: &'static [&'static str],
471    /// Set of crate names whose frames should be hidden when they appear
472    /// at the end of a backtrace.
473    pub skipped_final_crates: &'static [&'static str],
474    /// Maximum number of entries to include in the backtrace.
475    pub max_entry_count: usize,
476    /// Whether to show full file paths in the backtrace frames.
477    pub show_full_path: bool,
478}
479
480impl BacktraceFilter {
481    /// Default backtrace filter settings.
482    pub const DEFAULT: Self = Self {
483        skipped_initial_crates: &[
484            "backtrace",
485            "rootcause",
486            "rootcause-backtrace",
487            "core",
488            "std",
489            "alloc",
490        ],
491        skipped_middle_crates: &["std", "core", "alloc", "tokio"],
492        skipped_final_crates: &["std", "core", "alloc", "tokio"],
493        max_entry_count: 20,
494        show_full_path: false,
495    };
496}
497
498impl Default for BacktraceFilter {
499    fn default() -> Self {
500        Self::DEFAULT
501    }
502}
503
504#[derive(Debug)]
505struct RootcauseEnvOptions {
506    rust_backtrace_full: bool,
507    backtrace_leafs_only: bool,
508    show_full_path: bool,
509}
510
511impl RootcauseEnvOptions {
512    fn get() -> &'static Self {
513        static ROOTCAUSE_FLAGS: OnceLock<RootcauseEnvOptions> = OnceLock::new();
514
515        ROOTCAUSE_FLAGS.get_or_init(|| {
516            let rust_backtrace_full =
517                std::env::var_os("RUST_BACKTRACE").is_some_and(|var| var == "full");
518            let mut show_full_path = rust_backtrace_full;
519            let mut backtrace_leafs_only = false;
520            if let Some(var) = std::env::var_os("ROOTCAUSE_BACKTRACE") {
521                for v in var.to_string_lossy().split(',') {
522                    if v.eq_ignore_ascii_case("leafs") {
523                        backtrace_leafs_only = true;
524                    } else if v.eq_ignore_ascii_case("full_paths") {
525                        show_full_path = true;
526                    }
527                }
528            }
529            RootcauseEnvOptions {
530                rust_backtrace_full,
531                backtrace_leafs_only,
532                show_full_path,
533            }
534        })
535    }
536}
537
538impl BacktraceCollector {
539    /// Creates a new [`BacktraceCollector`] with default settings.
540    ///
541    /// Configuration is controlled by environment variables. By default,
542    /// filtering is applied and backtraces are only captured for reports
543    /// without children.
544    ///
545    /// # Environment Variables
546    ///
547    /// - `RUST_BACKTRACE=full` - Disables all filtering and shows all frames
548    /// - `ROOTCAUSE_BACKTRACE` - Comma-separated options:
549    ///   - `leafs` - Only capture backtraces for leaf errors (errors without
550    ///     children)
551    ///   - `full_paths` - Show full file paths instead of shortened paths
552    ///
553    /// The `RUST_BACKTRACE=full` setting implies `full_paths` unless explicitly
554    /// overridden by `ROOTCAUSE_BACKTRACE`.
555    ///
556    /// # Examples
557    ///
558    /// ```
559    /// use rootcause::hooks::Hooks;
560    /// use rootcause_backtrace::BacktraceCollector;
561    ///
562    /// // Respects RUST_BACKTRACE and ROOTCAUSE_BACKTRACE environment variables
563    /// Hooks::new()
564    ///     .report_creation_hook(BacktraceCollector::new_from_env())
565    ///     .install()
566    ///     .expect("failed to install hooks");
567    /// ```
568    pub fn new_from_env() -> Self {
569        let env_options = RootcauseEnvOptions::get();
570        let capture_backtrace_for_reports_with_children = !env_options.backtrace_leafs_only;
571
572        Self {
573            filter: if env_options.rust_backtrace_full {
574                BacktraceFilter {
575                    skipped_initial_crates: &[],
576                    skipped_middle_crates: &[],
577                    skipped_final_crates: &[],
578                    max_entry_count: usize::MAX,
579                    show_full_path: env_options.show_full_path,
580                }
581            } else {
582                BacktraceFilter {
583                    show_full_path: env_options.show_full_path,
584                    ..BacktraceFilter::DEFAULT
585                }
586            },
587            capture_backtrace_for_reports_with_children,
588        }
589    }
590}
591
592impl ReportCreationHook for BacktraceCollector {
593    fn on_local_creation(&self, mut report: ReportMut<'_, Dynamic, markers::Local>) {
594        let do_capture =
595            self.capture_backtrace_for_reports_with_children || report.children().is_empty();
596        if do_capture && let Some(backtrace) = Backtrace::capture(&self.filter) {
597            let attachment = if self.filter.show_full_path {
598                ReportAttachment::new_custom::<BacktraceHandler<true>>(backtrace)
599            } else {
600                ReportAttachment::new_custom::<BacktraceHandler<false>>(backtrace)
601            };
602            report.attachments_mut().push(attachment.into_dynamic());
603        }
604    }
605
606    fn on_sendsync_creation(&self, mut report: ReportMut<'_, Dynamic, markers::SendSync>) {
607        let do_capture =
608            self.capture_backtrace_for_reports_with_children || report.children().is_empty();
609        if do_capture && let Some(backtrace) = Backtrace::capture(&self.filter) {
610            let attachment = if self.filter.show_full_path {
611                ReportAttachment::new_custom::<BacktraceHandler<true>>(backtrace)
612            } else {
613                ReportAttachment::new_custom::<BacktraceHandler<false>>(backtrace)
614            };
615            report.attachments_mut().push(attachment.into_dynamic());
616        }
617    }
618}
619
620const fn get_rootcause_backtrace_matcher(
621    location: &'static Location<'static>,
622) -> Option<(&'static str, usize)> {
623    let file = location.file();
624
625    let Some(prefix_len) = file.len().checked_sub("/src/lib.rs".len()) else {
626        return None;
627    };
628
629    let (prefix, suffix) = file.split_at(prefix_len);
630    // Assert the suffix is /src/lib.rs (or \src\lib.rs on Windows)
631    // This is a compile-time check that the caller location is valid
632    if HOST_PATH_SEPARATOR == '/' {
633        assert!(suffix.eq_ignore_ascii_case("/src/lib.rs"));
634    } else {
635        assert!(suffix.eq_ignore_ascii_case(r#"\src\lib.rs"#));
636    }
637
638    let (matcher_prefix, _) = file.split_at(prefix_len + 4);
639
640    let mut splitter_prefix = prefix;
641    while !splitter_prefix.is_empty() {
642        let (new_prefix, last_char) = splitter_prefix.split_at(splitter_prefix.len() - 1);
643        splitter_prefix = new_prefix;
644        if last_char.eq_ignore_ascii_case(HOST_PATH_SEPARATOR.encode_utf8(&mut [0])) {
645            break;
646        }
647    }
648
649    Some((matcher_prefix, splitter_prefix.len()))
650}
651
652const HOST_PATH_SEPARATOR: char = include!(concat!(env!("OUT_DIR"), "/host_path_separator"));
653const ROOTCAUSE_BACKTRACE_MATCHER: Option<(&str, usize)> =
654    get_rootcause_backtrace_matcher(Location::caller());
655const ROOTCAUSE_MATCHER: Option<(&str, usize)> =
656    get_rootcause_backtrace_matcher(rootcause::__private::ROOTCAUSE_LOCATION);
657
658impl Backtrace {
659    /// Captures the current stack backtrace, applying optional filtering.
660    pub fn capture(filter: &BacktraceFilter) -> Option<Self> {
661        let mut initial_filtering = !filter.skipped_initial_crates.is_empty();
662        let mut entries: Vec<BacktraceEntry> = Vec::new();
663        let mut total_omitted_frames = 0;
664
665        let mut delayed_omitted_frame: Option<Frame> = None;
666        let mut currently_omitted_crate_name: Option<&'static str> = None;
667        let mut currently_omitted_frames = 0;
668
669        backtrace::trace(|frame| {
670            backtrace::resolve_frame(frame, |symbol| {
671                // Don't consider frames without symbol names or filenames.
672                let (Some(sym), Some(filename_raw)) = (symbol.name(), symbol.filename_raw()) else {
673                    return;
674                };
675
676                if entries.len() >= filter.max_entry_count {
677                    total_omitted_frames += 1;
678                    return;
679                }
680
681                let frame_path = FramePath::new(filename_raw);
682
683                if initial_filtering {
684                    if let Some(cur_crate_name) = &frame_path.crate_name
685                        && filter.skipped_initial_crates.contains(&&**cur_crate_name)
686                    {
687                        total_omitted_frames += 1;
688                        return;
689                    } else {
690                        initial_filtering = false;
691                    }
692                }
693
694                if let Some(cur_crate_name) = &frame_path.crate_name
695                    && let Some(currently_omitted_crate_name) = &currently_omitted_crate_name
696                    && cur_crate_name == currently_omitted_crate_name
697                {
698                    delayed_omitted_frame = None;
699                    currently_omitted_frames += 1;
700                    total_omitted_frames += 1;
701                    return;
702                }
703
704                if let Some(currently_omitted_crate_name) = currently_omitted_crate_name.take() {
705                    if let Some(delayed_frame) = delayed_omitted_frame.take() {
706                        entries.push(BacktraceEntry::Frame(delayed_frame));
707                    } else {
708                        entries.push(BacktraceEntry::OmittedFrames {
709                            count: currently_omitted_frames,
710                            skipped_crate: currently_omitted_crate_name,
711                        });
712                    }
713                    currently_omitted_frames = 0;
714                }
715
716                if let Some(cur_crate_name) = &frame_path.crate_name
717                    && let Some(skipped_crate) = filter
718                        .skipped_middle_crates
719                        .iter()
720                        .find(|&crate_name| crate_name == cur_crate_name)
721                {
722                    currently_omitted_crate_name = Some(skipped_crate);
723                    currently_omitted_frames = 1;
724                    total_omitted_frames += 1;
725                    delayed_omitted_frame = Some(Frame {
726                        sym_demangled: format!("{sym:#}"),
727                        frame_path: Some(frame_path),
728                        lineno: symbol.lineno(),
729                    });
730                    return;
731                }
732
733                entries.push(BacktraceEntry::Frame(Frame {
734                    sym_demangled: format!("{sym:#}"),
735                    frame_path: Some(frame_path),
736                    lineno: symbol.lineno(),
737                }));
738            });
739
740            true
741        });
742
743        if let Some(currently_omitted_crate_name) = currently_omitted_crate_name.take() {
744            if let Some(delayed_frame) = delayed_omitted_frame.take() {
745                entries.push(BacktraceEntry::Frame(delayed_frame));
746            } else {
747                entries.push(BacktraceEntry::OmittedFrames {
748                    count: currently_omitted_frames,
749                    skipped_crate: currently_omitted_crate_name,
750                });
751            }
752        }
753
754        while let Some(last) = entries.last() {
755            match last {
756                BacktraceEntry::Frame(frame) => {
757                    let mut skip = false;
758                    if let Some(frame_path) = &frame.frame_path
759                        && let Some(crate_name) = &frame_path.crate_name
760                        && filter.skipped_final_crates.contains(&&**crate_name)
761                    {
762                        skip = true;
763                    } else if frame.sym_demangled == "__libc_start_call_main"
764                        || frame.sym_demangled == "__libc_start_main_impl"
765                    {
766                        skip = true;
767                    } else if let Some(frame_path) = &frame.frame_path
768                        && frame.sym_demangled == "_start"
769                        && frame_path.raw_path.contains("zig/libc/glibc")
770                    {
771                        skip = true;
772                    }
773
774                    if skip {
775                        total_omitted_frames += 1;
776                        entries.pop();
777                    } else {
778                        break;
779                    }
780                }
781                BacktraceEntry::OmittedFrames {
782                    skipped_crate,
783                    count,
784                } => {
785                    if filter.skipped_final_crates.contains(skipped_crate) {
786                        total_omitted_frames += count;
787                        entries.pop();
788                    } else {
789                        break;
790                    }
791                }
792            }
793        }
794
795        if entries.is_empty() && total_omitted_frames == 0 {
796            None
797        } else {
798            Some(Self {
799                entries,
800                total_omitted_frames,
801            })
802        }
803    }
804}
805
806impl FramePath {
807    fn new(path: BytesOrWideString<'_>) -> Self {
808        static REGEXES: OnceLock<[regex::Regex; 2]> = OnceLock::new();
809        let [std_regex, registry_regex] = REGEXES.get_or_init(|| {
810            [
811                // Matches Rust standard library paths:
812                // - /lib/rustlib/src/rust/library/{std|core|alloc}/src/...
813                // - /rustc/{40-char-hash}/library/{std|core|alloc}/src/...
814                regex::Regex::new(
815                    r"(?:/lib/rustlib/src/rust|^/rustc/[0-9a-f]{40})/library/(std|core|alloc)/src/.*$",
816                )
817                .expect("built-in regex pattern for std library paths should be valid"),
818                // Matches Cargo registry paths:
819                // - /.cargo/registry/src/{index}-{16-char-hash}/{crate}-{version}/src/...
820                regex::Regex::new(
821                    r"/\.cargo/registry/src/[^/]+-[0-9a-f]{16}/([^./]+)-[0-9]+\.[^/]*/src/.*$",
822                )
823                .expect("built-in regex pattern for cargo registry paths should be valid"),
824            ]
825        });
826
827        let path_str = path.to_string();
828
829        if let Some(captures) = std_regex.captures(&path_str) {
830            let raw_path = path.to_str_lossy().into_owned();
831            let crate_capture = captures
832                .get(1)
833                .expect("regex capture group 1 should exist for std library paths");
834            let split = crate_capture.start();
835            let (prefix, suffix) = (&path_str[..split - 1], &path_str[split..]);
836
837            Self {
838                raw_path,
839                split_path: Some(FramePrefix {
840                    prefix_kind: "RUST_SRC",
841                    prefix: prefix.to_string(),
842                    suffix: suffix.to_string(),
843                }),
844                crate_name: Some(crate_capture.as_str().to_string().into()),
845            }
846        } else if let Some(captures) = registry_regex.captures(&path_str) {
847            let raw_path = path.to_str_lossy().into_owned();
848            let crate_capture = captures
849                .get(1)
850                .expect("regex capture group 1 should exist for cargo registry paths");
851            let split = crate_capture.start();
852            let (prefix, suffix) = (&path_str[..split - 1], &path_str[split..]);
853
854            Self {
855                raw_path,
856                crate_name: Some(crate_capture.as_str().to_string().into()),
857                split_path: Some(FramePrefix {
858                    prefix_kind: "CARGO",
859                    prefix: prefix.to_string(),
860                    suffix: suffix.to_string(),
861                }),
862            }
863        } else if let Some((rootcause_matcher_prefix, rootcause_splitter_prefix_len)) =
864            ROOTCAUSE_MATCHER
865            && path_str.starts_with(rootcause_matcher_prefix)
866        {
867            let raw_path = path.to_str_lossy().into_owned();
868            let (prefix, suffix) = (
869                &path_str[..rootcause_splitter_prefix_len],
870                &path_str[rootcause_splitter_prefix_len + 1..],
871            );
872            Self {
873                raw_path,
874                split_path: Some(FramePrefix {
875                    prefix_kind: "ROOTCAUSE",
876                    prefix: prefix.to_string(),
877                    suffix: suffix.to_string(),
878                }),
879                crate_name: Some(Cow::Borrowed("rootcause")),
880            }
881        } else if let Some((rootcause_matcher_prefix, rootcause_splitter_prefix_len)) =
882            ROOTCAUSE_BACKTRACE_MATCHER
883            && path_str.starts_with(rootcause_matcher_prefix)
884        {
885            let raw_path = path.to_str_lossy().into_owned();
886            let (prefix, suffix) = (
887                &path_str[..rootcause_splitter_prefix_len],
888                &path_str[rootcause_splitter_prefix_len + 1..],
889            );
890            Self {
891                raw_path,
892                split_path: Some(FramePrefix {
893                    prefix_kind: "ROOTCAUSE",
894                    prefix: prefix.to_string(),
895                    suffix: suffix.to_string(),
896                }),
897                crate_name: Some(Cow::Borrowed("rootcause-backtrace")),
898            }
899        } else {
900            let raw_path = path.to_str_lossy().into_owned();
901            Self {
902                raw_path,
903                crate_name: None,
904                split_path: None,
905            }
906        }
907    }
908}
909
910/// Extension trait for attaching backtraces to reports.
911///
912/// This trait provides methods to easily attach a captured backtrace to a
913/// report or to the error contained within a `Result`.
914///
915/// # Examples
916///
917/// Attach backtrace to a report:
918///
919/// ```
920/// use std::io;
921///
922/// use rootcause::report;
923/// use rootcause_backtrace::BacktraceExt;
924///
925/// let report = report!(io::Error::other("An error occurred")).attach_backtrace();
926/// ```
927///
928/// Attach backtrace to a `Result`:
929///
930/// ```
931/// use std::io;
932///
933/// use rootcause::{Report, report};
934/// use rootcause_backtrace::BacktraceExt;
935///
936/// fn might_fail() -> Result<(), Report> {
937///     Err(report!(io::Error::other("operation failed")).into_dynamic())
938/// }
939///
940/// let result = might_fail().attach_backtrace();
941/// ```
942///
943/// Use a custom filter:
944///
945/// ```
946/// use std::io;
947///
948/// use rootcause::report;
949/// use rootcause_backtrace::{BacktraceExt, BacktraceFilter};
950///
951/// let filter = BacktraceFilter {
952///     skipped_initial_crates: &[],
953///     skipped_middle_crates: &[],
954///     skipped_final_crates: &[],
955///     max_entry_count: 50,
956///     show_full_path: true,
957/// };
958///
959/// let report = report!(io::Error::other("detailed error")).attach_backtrace_with_filter(&filter);
960/// ```
961pub trait BacktraceExt: Sized {
962    /// Attaches a captured backtrace to the report using the default filter.
963    ///
964    /// # Examples
965    ///
966    /// ```
967    /// use std::io;
968    ///
969    /// use rootcause::report;
970    /// use rootcause_backtrace::BacktraceExt;
971    ///
972    /// let report = report!(io::Error::other("error")).attach_backtrace();
973    /// ```
974    fn attach_backtrace(self) -> Self {
975        self.attach_backtrace_with_filter(&BacktraceFilter::DEFAULT)
976    }
977
978    /// Attaches a captured backtrace to the report using the specified filter.
979    ///
980    /// # Examples
981    ///
982    /// ```
983    /// use std::io;
984    ///
985    /// use rootcause::report;
986    /// use rootcause_backtrace::{BacktraceExt, BacktraceFilter};
987    ///
988    /// let filter = BacktraceFilter {
989    ///     max_entry_count: 10,
990    ///     ..BacktraceFilter::DEFAULT
991    /// };
992    ///
993    /// let report = report!(io::Error::other("error")).attach_backtrace_with_filter(&filter);
994    /// ```
995    fn attach_backtrace_with_filter(self, filter: &BacktraceFilter) -> Self;
996}
997
998impl<C: ?Sized, T> BacktraceExt for Report<C, markers::Mutable, T>
999where
1000    Backtrace: ObjectMarkerFor<T>,
1001{
1002    fn attach_backtrace_with_filter(mut self, filter: &BacktraceFilter) -> Self {
1003        if let Some(backtrace) = Backtrace::capture(filter) {
1004            if filter.show_full_path {
1005                self = self.attach_custom::<BacktraceHandler<true>, _>(backtrace);
1006            } else {
1007                self = self.attach_custom::<BacktraceHandler<false>, _>(backtrace);
1008            }
1009        }
1010        self
1011    }
1012}
1013
1014impl<C: ?Sized, V, T> BacktraceExt for Result<V, Report<C, markers::Mutable, T>>
1015where
1016    Backtrace: ObjectMarkerFor<T>,
1017{
1018    fn attach_backtrace_with_filter(self, filter: &BacktraceFilter) -> Self {
1019        match self {
1020            Ok(v) => Ok(v),
1021            Err(report) => Err(report.attach_backtrace_with_filter(filter)),
1022        }
1023    }
1024}