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));
77
78#[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 version is unsupported")]
98 Version,
99
100 #[error("failure encoding has trailing bytes")]
101 Trailing,
102
103 #[error("failure encoding is malformed")]
104 Encoding(#[source] postcard::Error),
105}
106
107impl Failure {
112 pub fn new(code: operations::FailureKind, cause: impl Into<String>) -> Self {
113 Self(Box::new(Record { code, cause: cause.into(), context: Context::default(), diagnostics: None }))
114 }
115
116 pub fn originate(code: operations::FailureKind, error: &(dyn std::error::Error + 'static), origin: Origin) -> Self {
122 if let Some(carried) = carried(error) {
123 let mut forwarded = carried.clone();
124 forwarded.0.code = code;
125 return forwarded;
126 }
127
128 let chain: Vec<String> = sources(error)
130 .map(|error| match error.downcast_ref::<crate::Refusal>() {
131 Some(refusal) => refusal.message().to_owned(),
132 None => error.to_string(),
133 })
134 .collect();
135 let cause = chain.iter().rev().find(|message| !message.trim().is_empty()).cloned().unwrap_or_else(|| "operation failed".to_owned());
136
137 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 if version != VERSION {
194 return Err(CodecError::Version);
195 }
196 let (failure, rest) = postcard::take_from_bytes::<Self>(rest).map_err(CodecError::Encoding)?;
197 if !rest.is_empty() {
198 return Err(CodecError::Trailing);
199 }
200 Ok(failure)
201 }
202}
203
204impl fmt::Display for Failure {
205 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
206 formatter.write_str(&self.0.cause)
207 }
208}
209
210impl std::error::Error for Failure {}
211
212impl fmt::Display for Report<'_> {
213 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
214 let mut rendered = String::new();
215 for link in sources(self.0) {
216 let line = link.to_string();
217 if line.trim().is_empty() || rendered.contains(&line) {
218 continue;
219 }
220 if !rendered.is_empty() {
221 rendered.push_str(": ");
222 }
223 rendered.push_str(&line);
224 }
225 formatter.write_str(&rendered)
226 }
227}
228
229impl Context {
230 pub fn operation(&self) -> Option<&str> {
231 self.operation.as_deref()
232 }
233
234 pub fn step(&self) -> Option<&str> {
235 self.step.as_deref()
236 }
237}
238
239impl Diagnostics {
240 pub fn origin(&self) -> Origin {
241 self.origin
242 }
243
244 pub fn chain(&self) -> &[String] {
245 &self.chain
246 }
247
248 pub fn backtrace(&self) -> Option<&str> {
249 self.backtrace.as_deref()
250 }
251}
252
253fn sources<'a>(error: &'a (dyn std::error::Error + 'static)) -> impl Iterator<Item = &'a (dyn std::error::Error + 'static)> {
258 std::iter::successors(Some(error), |error| error.source())
259}
260
261fn carried<'a>(error: &'a (dyn std::error::Error + 'static)) -> Option<&'a Failure> {
262 sources(error)
263 .find_map(|error| error.downcast_ref::<Failure>().or_else(|| error.downcast_ref::<crate::Refusal>().map(crate::Refusal::failure)))
264}
265
266fn bounded(mut text: String, limit: usize) -> String {
267 if text.len() <= limit {
268 return text;
269 }
270 let mut end = limit;
271 while !text.is_char_boundary(end) {
272 end -= 1;
273 }
274 text.truncate(end);
275 text
276}
277
278