Skip to main content

human_panic/
report.rs

1//! This module encapsulates the report of a failure event.
2//!
3//! A `Report` contains the metadata collected about the event
4//! to construct a helpful error message.
5
6use 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/// Method of failure.
19#[derive(Debug, Serialize, Clone, Copy)]
20#[non_exhaustive]
21pub enum Method {
22    /// Failure caused by a panic.
23    Panic,
24}
25
26/// Contains metadata about the crash like the backtrace and
27/// information about the crate and operating system. Can
28/// be used to be serialized and persisted or printed as
29/// information to the user.
30#[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    /// Create a new instance.
43    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    /// Serialize the `Report` to a TOML string.
98    pub fn serialize(&self) -> Option<String> {
99        toml::to_string_pretty(&self).ok()
100    }
101
102    /// Write a file to disk.
103    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    //We take padding for address and extra two letters
116    //to pad after index.
117    #[allow(unused_qualifications)] // needed for pre-1.80 MSRV
118    const HEX_WIDTH: usize = mem::size_of::<usize>() * 2 + 2;
119    //Padding for next lines after frame's address
120    const NEXT_SYMBOL_PADDING: usize = HEX_WIDTH + 6;
121
122    let mut backtrace = String::new();
123
124    //Here we iterate over backtrace frames
125    //(each corresponds to function's stack)
126    //We need to print its address
127    //and symbol(e.g. function name),
128    //if it is available
129    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            //See if there is debug information with file name and line
163            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}