rootcause_backtrace/
lib.rs

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