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 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)
132 .map(|error| match error.downcast_ref::<crate::Refusal>() {
133 Some(refusal) => refusal.message().to_owned(),
134 None => error.to_string(),
135 })
136 .collect();
137 let cause = chain.iter().rev().find(|message| !message.trim().is_empty()).cloned().unwrap_or_else(|| "operation failed".to_owned());
138
139 let backtrace = Backtrace::capture();
141 let backtrace = (backtrace.status() == BacktraceStatus::Captured).then(|| bounded(backtrace.to_string(), BACKTRACE_LIMIT));
142 let chain = chain.into_iter().take(CHAIN_LIMIT).map(|message| bounded(message, LINK_LIMIT)).collect();
143 let diagnostics = Diagnostics { origin, chain, backtrace };
144
145 let mut failure = Self::new(code, cause);
146 failure.0.diagnostics = Some(diagnostics);
147 failure
148 }
149
150 pub fn find<'a>(error: &'a (dyn std::error::Error + 'static)) -> Option<&'a Self> {
152 carried(error)
153 }
154
155 pub fn code(&self) -> operations::FailureKind {
156 self.0.code
157 }
158
159 pub fn cause(&self) -> &str {
160 &self.0.cause
161 }
162
163 pub fn context(&self) -> &Context {
164 &self.0.context
165 }
166
167 pub fn diagnostics(&self) -> Option<&Diagnostics> {
168 self.0.diagnostics.as_ref()
169 }
170
171 pub fn in_operation(mut self, operation: impl Into<String>) -> Self {
174 self.0.context.operation.get_or_insert_with(|| operation.into());
175 self
176 }
177
178 pub fn in_step(mut self, step: impl Into<String>) -> Self {
180 self.0.context.step.get_or_insert_with(|| step.into());
181 self
182 }
183
184 pub fn without_diagnostics(mut self) -> Self {
185 self.0.diagnostics = None;
186 self
187 }
188
189 pub fn encode(&self) -> Vec<u8> {
190 postcard::to_allocvec(&(VERSION, self)).expect("a failure serializes to postcard")
191 }
192
193 pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
194 let (_version, rest) = postcard::take_from_bytes::<u8>(bytes).map_err(CodecError::Encoding)?;
195 let (failure, rest) = postcard::take_from_bytes::<Self>(rest).map_err(CodecError::Encoding)?;
196 if !rest.is_empty() {
197 return Err(CodecError::Trailing);
198 }
199 Ok(failure)
200 }
201}
202
203impl fmt::Display for Failure {
204 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
205 formatter.write_str(&self.0.cause)
206 }
207}
208
209impl std::error::Error for Failure {}
210
211impl fmt::Display for Report<'_> {
212 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
213 let mut rendered = String::new();
214 for link in sources(self.0) {
215 let line = link.to_string();
216 if line.trim().is_empty() || rendered.contains(&line) {
217 continue;
218 }
219 if !rendered.is_empty() {
220 rendered.push_str(": ");
221 }
222 rendered.push_str(&line);
223 }
224 formatter.write_str(&rendered)
225 }
226}
227
228impl Context {
229 pub fn operation(&self) -> Option<&str> {
230 self.operation.as_deref()
231 }
232
233 pub fn step(&self) -> Option<&str> {
234 self.step.as_deref()
235 }
236}
237
238impl Diagnostics {
239 pub fn origin(&self) -> Origin {
240 self.origin
241 }
242
243 pub fn chain(&self) -> &[String] {
244 &self.chain
245 }
246
247 pub fn backtrace(&self) -> Option<&str> {
248 self.backtrace.as_deref()
249 }
250}
251
252fn sources<'a>(error: &'a (dyn std::error::Error + 'static)) -> impl Iterator<Item = &'a (dyn std::error::Error + 'static)> {
257 std::iter::successors(Some(error), |error| error.source())
258}
259
260fn carried<'a>(error: &'a (dyn std::error::Error + 'static)) -> Option<&'a Failure> {
261 sources(error)
262 .find_map(|error| error.downcast_ref::<Failure>().or_else(|| error.downcast_ref::<crate::Refusal>().map(crate::Refusal::failure)))
263}
264
265fn bounded(mut text: String, limit: usize) -> String {
266 if text.len() <= limit {
267 return text;
268 }
269 let mut end = limit;
270 while !text.is_char_boundary(end) {
271 end -= 1;
272 }
273 text.truncate(end);
274 text
275}
276
277