Skip to main content

charon_error/report/
error_frame.rs

1use backtrace::Backtrace;
2use chrono::{DateTime, Utc};
3use colored::Colorize;
4use regex::Regex;
5use std::fmt::Display;
6use std::{collections::HashMap, error::Error};
7use tracing::{Metadata, Span, instrument};
8use tracing_error::SpanTrace;
9
10use crate::{
11    ErrorFmt, ErrorFmtLoD, ErrorFmtSettings, ErrorFormatObj, ResultER, ResultExt, SourceLocation,
12    StringError, map,
13};
14
15/// A single frame in an error chain, capturing one error with its full context.
16///
17/// Each `ErrorFrame` records:
18/// - The error message and original error object
19/// - Source location (file, line, column) via `#[track_caller]`
20/// - Timestamp of when the frame was created
21/// - Backtrace at the point of creation
22/// - Current `tracing` span and span trace
23/// - Arbitrary key-value attachments with sensitivity labels
24///
25/// Frames are collected inside an [`ErrorReport`](crate::ErrorReport).
26#[derive(Debug)]
27pub struct ErrorFrame {
28    /// Error message text, wrapped in a sensitivity label.
29    pub message: ErrorSensitivityLabel<String>,
30    /// The original error object.
31    pub error: Box<dyn Error + Send + Sync + 'static>,
32    /// Source code location where this frame was created.
33    pub source: SourceLocation,
34    /// Timestamp of when this error frame was created (UTC).
35    pub date_time: DateTime<Utc>,
36    /// Suggestions that could help resolve the error.
37    pub suggestions: Vec<ErrorSensitivityLabel<String>>,
38    /// Context/circumstances the error occurred in.
39    pub context: ErrorSensitivityLabel<String>,
40    /// Key-value attachments with sensitivity labels for additional diagnostic data.
41    pub attachments: HashMap<String, Box<ErrorSensitivityLabel<ErrorAttachment>>>,
42    /// Captured backtrace at the point this frame was created.
43    /// Should always be considered Internal sensitivity.
44    pub backtrace: Backtrace,
45    /// The current `tracing` Span when this frame was created.
46    /// Should always be considered Internal sensitivity.
47    /// This can be used instead of Backtrace for creating a stack.
48    pub span: Span,
49    /// The span trace captured from `tracing-error`.
50    pub span_trace: SpanTrace,
51}
52
53impl ErrorFmt for ErrorFrame {
54    #[instrument]
55    fn create_format_obj(&self, settings: ErrorFmtSettings) -> ResultER<ErrorFormatObj> {
56        let object = ErrorFormatObj::Object(map! {
57            "message" => ErrorFormatObj::ColorString(self.message.to_string().bright_red().bold()),
58            "source_location" => ErrorFormatObj::SourceLocation(self.source.clone()),
59            "span_trace" => self.span_trace.create_format_obj(settings)?,
60            "attachments" => self.attachments.create_format_obj(settings)?,
61            "date_time" => self.date_time.create_format_obj(settings)?,
62        });
63        Ok(match settings.level_of_detail {
64            ErrorFmtLoD::Compact => object.filter_object_fields(vec![
65                "message",
66                "source_location",
67                "span_trace",
68                "attachments",
69            ])?,
70            ErrorFmtLoD::SubmitReport => object.filter_object_fields(vec![
71                "message",
72                "source_location",
73                "span_trace",
74                "attachments",
75            ])?,
76            ErrorFmtLoD::Medium => object,
77            ErrorFmtLoD::Full | ErrorFmtLoD::Debug => object,
78        })
79    }
80}
81
82impl ErrorFrame {
83    /// Create a new error frame from any error type.
84    ///
85    /// Captures source location, backtrace, timestamp, and current tracing span.
86    #[must_use]
87    #[track_caller]
88    pub fn new<E: Error + Send + Sync + 'static>(error: E) -> Self {
89        Self {
90            message: ErrorSensitivityLabel::Public(error.to_string()),
91            error: Box::new(error),
92            source: SourceLocation::from_caller_location(),
93            date_time: Utc::now(),
94            suggestions: Vec::new(),
95            context: ErrorSensitivityLabel::Public(String::new()),
96            attachments: HashMap::new(),
97            backtrace: Backtrace::new_unresolved(),
98            span: Span::current(),
99            span_trace: SpanTrace::capture(),
100        }
101    }
102
103    /// Create a new error frame from a boxed dynamic error.
104    #[must_use]
105    #[track_caller]
106    pub fn from_dyn_error(error: Box<dyn Error + Send + Sync + 'static>) -> Self {
107        Self {
108            message: ErrorSensitivityLabel::Public(error.to_string()),
109            error,
110            source: SourceLocation::from_caller_location(),
111            date_time: Utc::now(),
112            suggestions: Vec::new(),
113            context: ErrorSensitivityLabel::Public(String::new()),
114            attachments: HashMap::new(),
115            backtrace: Backtrace::new_unresolved(),
116            span: Span::current(),
117            span_trace: SpanTrace::capture(),
118        }
119    }
120
121    /// Create an error frame from a plain message string.
122    ///
123    /// The message is labeled as [`Internal`](ErrorSensitivityLabel::Internal).
124    #[must_use]
125    #[track_caller]
126    pub fn from_message<S: Into<String>>(msg: S) -> Self {
127        let error = StringError::new(msg);
128        Self {
129            message: ErrorSensitivityLabel::Internal(error.to_string()),
130            error: Box::new(error),
131            source: SourceLocation::from_caller_location(),
132            date_time: Utc::now(),
133            suggestions: Vec::new(),
134            context: ErrorSensitivityLabel::Internal(String::new()),
135            attachments: HashMap::new(),
136            backtrace: Backtrace::new_unresolved(),
137            span: Span::current(),
138            span_trace: SpanTrace::capture(),
139        }
140    }
141
142    /// Get the error message as a string.
143    #[must_use]
144    pub fn get_error_title(&self) -> String {
145        self.message.to_string()
146    }
147
148    /// Get the list of tracing span metadata from the span trace.
149    #[must_use]
150    pub fn get_span_list(&self) -> Vec<&'static Metadata<'static>> {
151        let mut span_list = Vec::new();
152        self.span_trace.with_spans(|meta_data, _field_values| {
153            span_list.push(meta_data);
154            true
155        });
156        span_list
157    }
158
159    /// Resolve and format the backtrace as a human-readable string.
160    ///
161    /// Cleans up paths by removing the current directory prefix, cargo registry
162    /// paths, and rustlib paths to keep output concise.
163    #[must_use]
164    pub fn create_backtrace_string(&self) -> String {
165        let mut bt = self.backtrace.clone();
166        bt.resolve();
167        let frames = bt.frames();
168        let mut backtrace_string = String::new();
169        let max_line = 11;
170        let current_folder: String = std::env::current_dir().unwrap_error().display().to_string();
171        let cargo_registry_folder =
172            Regex::new(r"\/.cargo\/registry\/[^/]*\/[^/]*\/").unwrap_error();
173        let rustlib_folder =
174            Regex::new(r"\/.rustup\/toolchains\/[^/]*\/lib\/rustlib\/src\/").unwrap_error();
175        for (frame, line) in frames.iter().zip(0..max_line) {
176            let symbol = frame.symbols().first().unwrap_error();
177            let mut name = String::new();
178            if let Some(name_value) = &symbol.name() {
179                let full_name = name_value.to_string();
180                let parts: Vec<&str> = full_name.split("::").collect();
181                if parts.len() >= 3 {
182                    name = format!(
183                        "{}::{}",
184                        parts.get(parts.len() - 3).unwrap_or(&""),
185                        parts.get(parts.len() - 2).unwrap_or(&""),
186                    );
187                }
188            }
189            // Will not be set on some systems.
190            // see: https://docs.rs/backtrace/latest/backtrace/struct.Symbol.html
191            let mut filepath = String::new();
192            if let Some(filename) = &symbol.filename() {
193                let line_nr = &symbol.lineno().unwrap_or_default();
194                let path = filename.to_str().unwrap_error();
195                // Remove path to main repo
196                let (_, path) = path.split_at(
197                    path.rfind(&current_folder)
198                        .map(|pos| pos + current_folder.len() + 1) // add 1 for `/`
199                        .unwrap_or(0),
200                );
201                // Remove path to cargo registry
202                let (_, path) = path.split_at(
203                    cargo_registry_folder
204                        .find(path)
205                        .map(|m| m.end())
206                        .unwrap_or(0),
207                );
208                // Remove path to rustlib
209                let (_, path) =
210                    path.split_at(rustlib_folder.find(path).map(|m| m.end()).unwrap_or(0));
211                if !path.starts_with("/rustc/") {
212                    filepath = format!(" => {path}:{line_nr}");
213                }
214            }
215            if line != 0 {
216                backtrace_string = format!("{backtrace_string}{line}: {name}{filepath}\n");
217            }
218        }
219        backtrace_string
220    }
221}
222
223/// Data that can be attached to an [`ErrorFrame`] for additional diagnostics.
224///
225/// Attachments carry either raw bytes or a string value, and are always
226/// wrapped in an [`ErrorSensitivityLabel`] to control their visibility.
227#[derive(Debug, Clone)]
228pub enum ErrorAttachment {
229    /// Raw binary data (displayed as `<raw_data>` in output).
230    RawData(Vec<u8>),
231    /// A human-readable string value.
232    String(String),
233}
234
235impl Display for ErrorAttachment {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        match self {
238            Self::RawData(_data) => write!(f, "<raw_data>"),
239            Self::String(data) => write!(f, "{data}"),
240        }
241    }
242}
243
244/// Controls the visibility and handling of data based on its sensitivity.
245///
246/// Each variant wraps a value of type `T` and determines who can see that data
247/// and whether it should be encrypted.
248///
249/// Data Owner: Data that should only be shared with the user, not other users.
250///
251/// Data Can be shared with:
252/// | Level              | Data Owner | Other Users | Internal | Encrypted        |
253/// |--------------------|:----------:|:-----------:|:--------:|:----------------:|
254/// | Public             |     yes    |     yes     |    yes   |       no         |
255/// | Private            |     yes    |      no     |    yes   |       no         |
256/// | PrivateConfidential|     yes    |      no     |     no   | yes (user key)   |
257/// |--------------------|:----------:|:-----------:|:--------:|:----------------:|
258/// | Internal           |      no    |      no     |    yes   |       no         |
259/// | Confidential       |      no    |      no     |    yes   | yes (app key)    |
260/// | HighlyConfidential |      no    |      no     |    yes   | yes (specific)   |
261#[derive(Debug, Clone)]
262pub enum ErrorSensitivityLabel<T> {
263    /// Data that can be shared with any user, no restrictions.
264    Public(T),
265    /// Data that should only be shared with the user who owns the data, no other users.
266    /// The data can be shared using Internal channels.
267    Private(T),
268    /// Data that should only be shared with the user, no other users.
269    /// The data can NOT be shared using Internal channels.
270    /// TODO: Data should be Encrypted using users public key.
271    PrivateConfidential(T),
272
273    /// Data that should only be shared Internally within the organization.
274    /// TODO: Data is should never be shared outside of organization.
275    Internal(T),
276    /// Data is considered sensitive and access is limited.
277    /// TODO: Data should be Encrypted using application public key.
278    Confidential(T),
279    /// Data is Very sensitive, and should not be stored unless encrypted.
280    /// TODO: Data is/should always be Encrypted using highly specific keys.
281    /// Data is only available in specific circumstances:
282    ///  - Only in Debug Builds
283    ///  - Never Stored in JSON
284    ///  - Printed as encrypted string
285    HighlyConfidential(T),
286}
287
288impl<T: Display> Display for ErrorSensitivityLabel<T> {
289    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290        let result = match self {
291            ErrorSensitivityLabel::Public(t) => t.to_string(),
292            ErrorSensitivityLabel::Private(_) => "Data is private".to_owned(),
293            ErrorSensitivityLabel::PrivateConfidential(_) => "Data is private".to_owned(),
294            ErrorSensitivityLabel::Internal(_) => "Data is only for internal use".to_owned(),
295            ErrorSensitivityLabel::Confidential(_) => "Data is only for internal use".to_owned(),
296            ErrorSensitivityLabel::HighlyConfidential(_) => {
297                "Data is only for internal use".to_owned()
298            }
299        };
300        write!(f, "{result}")
301    }
302}