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(¤t_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}