1use std::error::Error;
7use std::fmt::Write as FmtWrite;
8use std::mem;
9use std::panic::PanicHookInfo;
10use std::{env, path::Path, path::PathBuf};
11
12use backtrace::Backtrace;
13use serde_derive::Serialize;
14use uuid::Uuid;
15
16use crate::Metadata;
17
18#[derive(Debug, Serialize, Clone, Copy)]
20#[non_exhaustive]
21pub enum Method {
22 Panic,
24}
25
26#[derive(Debug, Serialize)]
31pub struct Report {
32 name: String,
33 operating_system: String,
34 crate_version: String,
35 explanation: String,
36 cause: String,
37 method: Method,
38 backtrace: String,
39}
40
41impl Report {
42 pub fn new(
44 name: &str,
45 version: &str,
46 method: Method,
47 explanation: String,
48 cause: String,
49 ) -> Self {
50 let cpu_arch = sysinfo::System::cpu_arch();
51 let operating_system =
52 sysinfo::System::long_os_version().unwrap_or_else(|| "unknown".to_owned());
53 let operating_system = format!("{operating_system} [{cpu_arch}]");
54 let backtrace = render_backtrace();
55
56 Self {
57 crate_version: version.into(),
58 name: name.into(),
59 operating_system,
60 method,
61 explanation,
62 cause,
63 backtrace,
64 }
65 }
66
67 #[allow(deprecated)]
68 pub fn with_panic(meta: &Metadata, panic_info: &PanicHookInfo<'_>) -> Self {
69 let mut expl = String::new();
70
71 let message = match (
72 panic_info.payload().downcast_ref::<&str>(),
73 panic_info.payload().downcast_ref::<String>(),
74 ) {
75 (Some(s), _) => Some((*s).to_owned()),
76 (_, Some(s)) => Some(s.to_owned()),
77 (None, None) => None,
78 };
79
80 let cause = match message {
81 Some(m) => m,
82 None => "Unknown".into(),
83 };
84
85 match panic_info.location() {
86 Some(location) => expl.push_str(&format!(
87 "Panic occurred in file '{}' at line {}\n",
88 location.file(),
89 location.line()
90 )),
91 None => expl.push_str("Panic location unknown.\n"),
92 }
93
94 Self::new(&meta.name, &meta.version, Method::Panic, expl, cause)
95 }
96
97 pub fn serialize(&self) -> Option<String> {
99 toml::to_string_pretty(&self).ok()
100 }
101
102 pub fn persist(&self) -> Result<PathBuf, Box<dyn Error + 'static>> {
104 let uuid = Uuid::new_v4().hyphenated().to_string();
105 let tmp_dir = env::temp_dir();
106 let file_name = format!("report-{}.toml", &uuid);
107 let file_path = Path::new(&tmp_dir).join(file_name);
108 let toml = self.serialize().expect("only using toml-compatible types");
109 std::fs::write(&file_path, toml.as_bytes())?;
110 Ok(file_path)
111 }
112}
113
114fn render_backtrace() -> String {
115 #[allow(unused_qualifications)] const HEX_WIDTH: usize = mem::size_of::<usize>() * 2 + 2;
119 const NEXT_SYMBOL_PADDING: usize = HEX_WIDTH + 6;
121
122 let mut backtrace = String::new();
123
124 let bt = Backtrace::new();
130 let symbols = bt
131 .frames()
132 .iter()
133 .flat_map(|frame| {
134 let symbols = frame.symbols();
135 if symbols.is_empty() {
136 vec![(frame, None, "<unresolved>".to_owned())]
137 } else {
138 symbols
139 .iter()
140 .map(|s| {
141 (
142 frame,
143 Some(s),
144 s.name()
145 .map(|n| n.to_string())
146 .unwrap_or_else(|| "<unknown>".to_owned()),
147 )
148 })
149 .collect::<Vec<_>>()
150 }
151 })
152 .collect::<Vec<_>>();
153 let begin_unwind = "rust_begin_unwind";
154 let begin_unwind_start = symbols
155 .iter()
156 .position(|(_, _, n)| n == begin_unwind)
157 .unwrap_or(0);
158 for (entry_idx, (frame, symbol, name)) in symbols.iter().skip(begin_unwind_start).enumerate() {
159 let ip = frame.ip();
160 let _ = writeln!(backtrace, "{entry_idx:4}: {ip:HEX_WIDTH$?} - {name}");
161 if let Some(symbol) = symbol {
162 if let (Some(file), Some(line)) = (symbol.filename(), symbol.lineno()) {
164 let _ = writeln!(
165 backtrace,
166 "{:3$}at {}:{}",
167 "",
168 file.display(),
169 line,
170 NEXT_SYMBOL_PADDING
171 );
172 }
173 }
174 }
175
176 backtrace
177}