1use std::backtrace::{Backtrace, BacktraceStatus};
24use std::fmt;
25use serde::{Deserialize, Serialize};
27use crate::operations;
29
30const LINK_LIMIT: usize = 4 * 1024;
33
34const CHAIN_LIMIT: usize = 16;
35
36const BACKTRACE_LIMIT: usize = 32 * 1024;
37
38const VERSION: u8 = 1;
39
40#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(transparent)]
49pub struct Failure(Box<Record>);
50
51#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(deny_unknown_fields)]
53struct Record {
54 code: operations::FailureKind,
55 cause: String,
56 context: Context,
57 diagnostics: Option<Diagnostics>,
58}
59
60#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(deny_unknown_fields)]
64pub struct Context {
65 operation: Option<String>,
66 step: Option<String>,
67}
68
69pub struct Report<'a>(pub &'a (dyn std::error::Error + 'static));
73
74#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(deny_unknown_fields)]
80pub struct Diagnostics {
81 origin: Origin,
82 chain: Vec<String>,
83 backtrace: Option<String>,
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum Origin {
91 Plugin,
92 Host,
93}
94
95#[derive(Debug, thiserror::Error)]
96pub enum CodecError {
97 #[error("failure encoding has trailing bytes")]
98 Trailing,
99
100 #[error("failure encoding is malformed")]
101 Encoding(#[source] postcard::Error),
102}
103
104impl Failure {
109 pub fn new(code: operations::FailureKind, cause: impl Into<String>) -> Self {
110 Self(Box::new(Record {
111 code,
112 cause: cause.into(),
113 context: Context::default(),
114 diagnostics: None,
115 }))
116 }
117
118 pub fn originate(code: operations::FailureKind, error: &(dyn std::error::Error + 'static), origin: Origin) -> Self {
124 if let Some(carried) = carried(error) {
125 let mut forwarded = carried.clone();
126 forwarded.0.code = code;
127 return forwarded;
128 }
129
130 let chain: Vec<String> = sources(error)
131 .map(|error| match error.downcast_ref::<crate::Refusal>() {
132 Some(refusal) => refusal.message().to_owned(),
133 None => error.to_string(),
134 })
135 .collect();
136 let cause = chain.iter().rev().find(|message| !message.trim().is_empty()).cloned().unwrap_or_else(|| "operation failed".to_owned());
137
138 let backtrace = Backtrace::capture();
139 let backtrace = (backtrace.status() == BacktraceStatus::Captured).then(|| bounded(backtrace.to_string(), BACKTRACE_LIMIT));
140 let chain = chain.into_iter().take(CHAIN_LIMIT).map(|message| bounded(message, LINK_LIMIT)).collect();
141 let diagnostics = Diagnostics { origin, chain, backtrace };
142
143 let mut failure = Self::new(code, cause);
144 failure.0.diagnostics = Some(diagnostics);
145 failure
146 }
147
148 pub fn find<'a>(error: &'a (dyn std::error::Error + 'static)) -> Option<&'a Self> {
150 carried(error)
151 }
152
153 pub fn code(&self) -> operations::FailureKind {
154 self.0.code
155 }
156
157 pub fn cause(&self) -> &str {
158 &self.0.cause
159 }
160
161 pub fn context(&self) -> &Context {
162 &self.0.context
163 }
164
165 pub fn diagnostics(&self) -> Option<&Diagnostics> {
166 self.0.diagnostics.as_ref()
167 }
168
169 pub fn in_operation(mut self, operation: impl Into<String>) -> Self {
172 self.0.context.operation.get_or_insert_with(|| operation.into());
173 self
174 }
175
176 pub fn in_step(mut self, step: impl Into<String>) -> Self {
178 self.0.context.step.get_or_insert_with(|| step.into());
179 self
180 }
181
182 pub fn without_diagnostics(mut self) -> Self {
183 self.0.diagnostics = None;
184 self
185 }
186
187 pub fn encode(&self) -> Vec<u8> {
188 postcard::to_allocvec(&(VERSION, self)).expect("a failure serializes to postcard")
189 }
190
191 pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
192 let (_version, rest) = postcard::take_from_bytes::<u8>(bytes).map_err(CodecError::Encoding)?;
193 let (failure, rest) = postcard::take_from_bytes::<Self>(rest).map_err(CodecError::Encoding)?;
194 if !rest.is_empty() {
195 return Err(CodecError::Trailing);
196 }
197 Ok(failure)
198 }
199}
200
201impl fmt::Display for Failure {
202 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
203 formatter.write_str(&self.0.cause)
204 }
205}
206
207impl std::error::Error for Failure {}
208
209impl fmt::Display for Report<'_> {
210 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
211 let mut rendered = String::new();
212 for link in sources(self.0) {
213 let line = link.to_string();
214 if line.trim().is_empty() || rendered.contains(&line) {
215 continue;
216 }
217 if !rendered.is_empty() {
218 rendered.push_str(": ");
219 }
220 rendered.push_str(&line);
221 }
222 formatter.write_str(&rendered)
223 }
224}
225
226impl Context {
227 pub fn operation(&self) -> Option<&str> {
228 self.operation.as_deref()
229 }
230
231 pub fn step(&self) -> Option<&str> {
232 self.step.as_deref()
233 }
234}
235
236impl Diagnostics {
237 pub fn origin(&self) -> Origin {
238 self.origin
239 }
240
241 pub fn chain(&self) -> &[String] {
242 &self.chain
243 }
244
245 pub fn backtrace(&self) -> Option<&str> {
246 self.backtrace.as_deref()
247 }
248}
249
250fn sources<'a>(error: &'a (dyn std::error::Error + 'static)) -> impl Iterator<Item = &'a (dyn std::error::Error + 'static)> {
255 std::iter::successors(Some(error), |error| error.source())
256}
257
258fn carried<'a>(error: &'a (dyn std::error::Error + 'static)) -> Option<&'a Failure> {
259 sources(error)
260 .find_map(|error| error.downcast_ref::<Failure>().or_else(|| error.downcast_ref::<crate::Refusal>().map(crate::Refusal::failure)))
261}
262
263fn bounded(mut text: String, limit: usize) -> String {
264 if text.len() <= limit {
265 return text;
266 }
267 let mut end = limit;
268 while !text.is_char_boundary(end) {
269 end -= 1;
270 }
271 text.truncate(end);
272 text
273}
274
275