Skip to main content

charon_error/report/
error_frame.rs

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