oopsie_core/erased/
backtrace.rs1use 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#[derive(Clone, Debug, Serialize, Deserialize)]
14#[non_exhaustive]
15pub struct ErasedBacktrace {
16 frames: Box<[ErasedFrame]>,
17}
18
19#[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 #[must_use]
32 #[inline]
33 pub fn name(&self) -> Option<&str> {
34 self.name.as_deref()
35 }
36
37 #[must_use]
39 #[inline]
40 pub fn filename(&self) -> Option<&str> {
41 self.filename.as_deref()
42 }
43
44 #[must_use]
46 #[inline]
47 pub const fn line(&self) -> Option<u32> {
48 self.line
49 }
50
51 #[must_use]
53 #[inline]
54 pub const fn column(&self) -> Option<u32> {
55 self.column
56 }
57}
58
59impl ErasedBacktrace {
60 #[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 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 #[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 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 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 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}