Skip to main content

oopsie_core/erased/
backtrace.rs

1//! Serializable backtrace representation.
2
3use alloc::boxed::Box;
4#[cfg(feature = "std")]
5use alloc::string::ToString as _;
6#[cfg(feature = "std")]
7use alloc::vec::Vec;
8use core::fmt;
9
10use serde::{Deserialize, Serialize};
11
12/// A serializable, type-erased representation of a backtrace.
13#[derive(Clone, Debug, Serialize, Deserialize)]
14#[non_exhaustive]
15pub struct ErasedBacktrace {
16    frames: Box<[ErasedFrame]>,
17}
18
19/// A single frame in an erased backtrace.
20#[derive(Clone, Debug, Serialize, Deserialize)]
21#[non_exhaustive]
22pub struct ErasedFrame {
23    name: Option<Box<str>>,
24    filename: Option<Box<str>>,
25    line: Option<u32>,
26    column: Option<u32>,
27}
28
29impl ErasedFrame {
30    /// The resolved symbol name, if any.
31    #[must_use]
32    #[inline]
33    pub fn name(&self) -> Option<&str> {
34        self.name.as_deref()
35    }
36
37    /// The source file the frame resolved to, if any.
38    #[must_use]
39    #[inline]
40    pub fn filename(&self) -> Option<&str> {
41        self.filename.as_deref()
42    }
43
44    /// The line within the source file, if any.
45    #[must_use]
46    #[inline]
47    pub const fn line(&self) -> Option<u32> {
48        self.line
49    }
50
51    /// The column within the line, if any.
52    #[must_use]
53    #[inline]
54    pub const fn column(&self) -> Option<u32> {
55        self.column
56    }
57}
58
59impl ErasedBacktrace {
60    /// Create an `ErasedBacktrace` from a live `Backtrace`.
61    ///
62    /// Captures every resolved symbol verbatim — including capture machinery,
63    /// OS/libc entry points, and unresolvable frames. Hiding
64    /// implementation/platform detail is a render-time concern; this snapshot
65    /// stays raw so a consumer can still render the full stack later.
66    #[cfg(feature = "std")]
67    #[must_use]
68    pub fn from_backtrace(bt: &crate::Backtrace) -> Self {
69        bt.resolve();
70
71        let mut frames = Vec::new();
72        for frame in bt.frames() {
73            let symbols = frame.symbols();
74            // A frame that failed to resolve has no symbols; keep a placeholder
75            // so the stack shape survives erasure.
76            if symbols.is_empty() {
77                frames.push(ErasedFrame {
78                    name: None,
79                    filename: None,
80                    line: None,
81                    column: None,
82                });
83            }
84            frames.extend(symbols.iter().map(|sym| ErasedFrame {
85                name: sym.name().map(|n| n.to_string().into_boxed_str()),
86                filename: sym.filename().map(|p| p.to_string_lossy().into()),
87                line: sym.lineno(),
88                column: sym.colno(),
89            }));
90        }
91        Self {
92            frames: frames.into(),
93        }
94    }
95
96    /// Returns a slice of all frames.
97    #[must_use]
98    #[inline]
99    pub fn frames(&self) -> &[ErasedFrame] {
100        &self.frames
101    }
102}
103
104#[cfg(feature = "std")]
105impl From<&crate::Backtrace> for ErasedBacktrace {
106    #[inline]
107    fn from(bt: &crate::Backtrace) -> Self {
108        Self::from_backtrace(bt)
109    }
110}
111#[cfg(feature = "std")]
112impl From<crate::Backtrace> for ErasedBacktrace {
113    #[inline]
114    fn from(bt: crate::Backtrace) -> Self {
115        Self::from_backtrace(&bt)
116    }
117}
118
119impl fmt::Display for ErasedBacktrace {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        for (i, frame) in self.frames.iter().enumerate() {
122            write!(f, "{:>3}: ", i + 1)?;
123            if let Some(name) = &frame.name {
124                writeln!(f, "{name}")?;
125            } else {
126                writeln!(f, "<unknown>")?;
127            }
128            if let Some(filename) = &frame.filename {
129                write!(f, "           at {filename}")?;
130                if let Some(line) = frame.line {
131                    write!(f, ":{line}")?;
132                    if let Some(col) = frame.column {
133                        write!(f, ":{col}")?;
134                    }
135                }
136                writeln!(f)?;
137            }
138        }
139        Ok(())
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    fn frame(
148        name: Option<&str>,
149        filename: Option<&str>,
150        line: Option<u32>,
151        column: Option<u32>,
152    ) -> ErasedFrame {
153        ErasedFrame {
154            name: name.map(|s| s.to_owned().into_boxed_str()),
155            filename: filename.map(Into::into),
156            line,
157            column,
158        }
159    }
160
161    fn single(f: ErasedFrame) -> ErasedBacktrace {
162        ErasedBacktrace {
163            frames: Box::from([f]),
164        }
165    }
166
167    #[test]
168    fn display_unknown_frame_without_location() {
169        // name == None -> `<unknown>`; filename == None -> no `at` line.
170        let bt = single(frame(None, None, None, None));
171        assert_eq!(bt.to_string(), "  1: <unknown>\n");
172    }
173
174    #[test]
175    fn display_full_location_with_column() {
176        let bt = single(frame(Some("a::b"), Some("/x/y.rs"), Some(12), Some(3)));
177        assert_eq!(bt.to_string(), "  1: a::b\n           at /x/y.rs:12:3\n");
178    }
179
180    #[test]
181    fn display_location_with_line_no_column() {
182        let bt = single(frame(Some("a::b"), Some("/x/y.rs"), Some(12), None));
183        assert_eq!(bt.to_string(), "  1: a::b\n           at /x/y.rs:12\n");
184    }
185
186    #[test]
187    fn display_location_with_filename_no_line() {
188        // filename present but no line -> bare path, no trailing `:`.
189        let bt = single(frame(Some("a::b"), Some("/x/y.rs"), None, None));
190        assert_eq!(bt.to_string(), "  1: a::b\n           at /x/y.rs\n");
191    }
192
193    #[test]
194    fn from_backtrace_retains_raw_internal_frames() {
195        crate::set_rust_backtrace_override(crate::RustBacktrace::Enabled);
196        let bt = <crate::Backtrace as crate::Capturable>::capture();
197        crate::clear_rust_backtrace_override();
198
199        let erased = ErasedBacktrace::from_backtrace(&bt);
200
201        // The capture path itself runs through this crate, so a raw snapshot
202        // must retain its frame — proving filtering is deferred to render
203        // time rather than applied here.
204        assert!(
205            erased.frames().iter().any(|fr| {
206                fr.filename
207                    .as_deref()
208                    .is_some_and(|f| f.starts_with(crate::__private::CORE_SRC_PATH))
209            }),
210            "from_backtrace should retain raw internal frames, not strip them at capture"
211        );
212    }
213
214    #[test]
215    fn from_backtrace_keeps_at_least_one_erased_frame_per_source_frame() {
216        crate::set_rust_backtrace_override(crate::RustBacktrace::Enabled);
217        let bt = <crate::Backtrace as crate::Capturable>::capture();
218        crate::clear_rust_backtrace_override();
219
220        let erased = ErasedBacktrace::from_backtrace(&bt);
221        assert!(
222            erased.frames().len() >= bt.frames().len(),
223            "every source frame must survive erasure (unresolved ones as placeholders): \
224             {} erased < {} source",
225            erased.frames().len(),
226            bt.frames().len()
227        );
228    }
229}