use derive_more::derive::Display;
use ecow::{EcoString, EcoVec};
use std::{
backtrace::{Backtrace, BacktraceStatus},
convert::Into,
fmt::{self, Display},
};
pub(crate) mod constants {
pub const DEFAULT_DOCS_URL: &str = "https://rustic.cli.rs/docs/errors/";
pub const DEFAULT_ISSUE_URL: &str = "https://github.com/rustic-rs/rustic_core/issues/new";
}
pub type RusticResult<T, E = Box<RusticError>> = Result<T, E>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
pub enum Severity {
Info,
Warning,
Error,
Fatal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
pub enum Status {
Permanent,
Temporary,
Persistent,
}
#[non_exhaustive]
#[derive(thiserror::Error, Debug, displaydoc::Display, Default, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
AppendOnly,
Backend,
Configuration,
Cryptography,
ExternalCommand,
Internal,
InvalidInput,
InputOutput,
Key,
MissingInput,
#[default]
Other,
Credentials,
Repository,
Unsupported,
Verification,
Vfs,
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub struct RusticError {
kind: ErrorKind,
guidance: EcoString,
docs_url: Option<EcoString>,
error_code: Option<EcoString>,
ask_report: bool,
existing_issue_urls: EcoVec<EcoString>,
new_issue_url: Option<EcoString>,
context: EcoVec<(EcoString, EcoString)>,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
severity: Option<Severity>,
status: Option<Status>,
backtrace: Option<Backtrace>,
}
impl Display for RusticError {
#[allow(clippy::too_many_lines)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"`rustic_core` experienced an error related to `{}`.",
self.kind
)?;
writeln!(f)?;
writeln!(f, "Message:")?;
let context = if self.context.is_empty() {
writeln!(f, "{}", self.guidance)?;
Vec::new()
} else {
let mut guidance = self.guidance.to_string();
let context = self
.context
.iter()
.filter(|(key, value)| {
let pattern = "{".to_owned() + key + "}";
if guidance.contains(&pattern) {
guidance = guidance.replace(&pattern, value);
false
} else {
true
}
})
.collect();
writeln!(f, "{guidance}")?;
context
};
if let Some(code) = &self.error_code {
let default_docs_url = EcoString::from(constants::DEFAULT_DOCS_URL);
let docs_url = self
.docs_url
.as_ref()
.unwrap_or(&default_docs_url)
.to_string();
let docs_url = if docs_url.ends_with('/') {
docs_url
} else {
docs_url + "/"
};
writeln!(f)?;
writeln!(f, "For more information, see: {docs_url}{code}")?;
}
if !self.existing_issue_urls.is_empty() {
writeln!(f)?;
writeln!(f, "Related issues:")?;
self.existing_issue_urls
.iter()
.try_for_each(|url| writeln!(f, "- {url}"))?;
}
if self.ask_report {
let default_issue_url = EcoString::from(constants::DEFAULT_ISSUE_URL);
let new_issue_url = self.new_issue_url.as_ref().unwrap_or(&default_issue_url);
writeln!(f)?;
writeln!(
f,
"We believe this is a bug, please report it by opening an issue at:"
)?;
writeln!(f, "{new_issue_url}")?;
writeln!(f)?;
writeln!(
f,
"If you can, please attach an anonymized debug log to the issue."
)?;
writeln!(f)?;
writeln!(f, "Thank you for helping us improve rustic!")?;
}
writeln!(f)?;
writeln!(f)?;
writeln!(f, "Some additional details ...")?;
if !context.is_empty() {
writeln!(f)?;
writeln!(f, "Context:")?;
context
.iter()
.try_for_each(|(key, value)| writeln!(f, "- {key}: {value}"))?;
}
if let Some(cause) = &self.source {
writeln!(f)?;
writeln!(f, "Caused by:")?;
writeln!(f, "{cause}")?;
if let Some(source) = cause.source() {
write!(f, " : (source: {source})")?;
}
writeln!(f)?;
}
if let Some(severity) = &self.severity {
writeln!(f)?;
writeln!(f, "Severity: {severity}")?;
}
if let Some(status) = &self.status {
writeln!(f)?;
writeln!(f, "Status: {status}")?;
}
if let Some(backtrace) = &self.backtrace {
writeln!(f)?;
writeln!(f, "Backtrace:")?;
write!(f, "{backtrace}")?;
if backtrace.status() == BacktraceStatus::Disabled {
writeln!(
f,
" (set 'RUST_BACKTRACE=\"1\"' environment variable to enable)"
)?;
}
}
Ok(())
}
}
impl RusticError {
pub fn new(kind: ErrorKind, guidance: impl Into<EcoString>) -> Box<Self> {
Box::new(Self {
kind,
guidance: guidance.into(),
context: EcoVec::default(),
source: None,
error_code: None,
docs_url: None,
new_issue_url: None,
existing_issue_urls: EcoVec::default(),
severity: None,
status: None,
ask_report: false,
backtrace: Some(Backtrace::capture()),
})
}
pub fn with_source(
kind: ErrorKind,
guidance: impl Into<EcoString>,
source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
) -> Box<Self> {
Self::new(kind, guidance).attach_source(source)
}
pub fn is_code(&self, code: &str) -> bool {
self.error_code.as_ref().is_some_and(|c| c.as_str() == code)
}
pub fn is_incorrect_password(&self) -> bool {
self.is_code("C002")
}
pub fn from<T: std::error::Error + Display + Send + Sync + 'static>(
kind: ErrorKind,
error: T,
) -> Box<Self> {
Self::with_source(kind, error.to_string(), error)
}
pub fn display_log(&self) -> String {
use std::fmt::Write;
let mut fmt = String::new();
_ = write!(fmt, "Error: ");
if self.context.is_empty() {
_ = write!(fmt, "{}", self.guidance);
} else {
let mut guidance = self.guidance.to_string();
self.context
.iter()
.for_each(|(key, value)| {
let pattern = "{".to_owned() + key + "}";
guidance = guidance.replace(&pattern, value);
});
_ = write!(fmt, "{guidance}");
}
_ = write!(fmt, " (kind: related to {}", self.kind);
if let Some(code) = &self.error_code {
_ = write!(fmt, ", code: {code}");
}
_ = write!(fmt, ")");
if let Some(cause) = &self.source {
_ = write!(fmt, ": caused by: {cause}");
if let Some(source) = cause.source() {
_ = write!(fmt, " : (source: {source})");
}
}
fmt
}
}
impl RusticError {
pub fn overwrite_kind(self, value: impl Into<ErrorKind>) -> Box<Self> {
Box::new(Self {
kind: value.into(),
..self
})
}
pub fn ask_report(self) -> Box<Self> {
Box::new(Self {
ask_report: true,
..self
})
}
pub fn attach_source(
self,
value: impl Into<Box<dyn std::error::Error + Send + Sync>>,
) -> Box<Self> {
Box::new(Self {
source: Some(value.into()),
..self
})
}
pub fn overwrite_guidance(self, value: impl Into<EcoString>) -> Box<Self> {
Box::new(Self {
guidance: value.into(),
..self
})
}
pub fn append_guidance_line(self, value: impl Into<EcoString>) -> Box<Self> {
Box::new(Self {
guidance: format!("{}\n{}", self.guidance, value.into()).into(),
..self
})
}
pub fn prepend_guidance_line(self, value: impl Into<EcoString>) -> Box<Self> {
Box::new(Self {
guidance: format!("{}\n{}", value.into(), self.guidance).into(),
..self
})
}
pub fn attach_context(
mut self,
key: impl Into<EcoString>,
value: impl Into<EcoString>,
) -> Box<Self> {
self.context.push((key.into(), value.into()));
Box::new(self)
}
pub fn overwrite_context(self, value: impl Into<EcoVec<(EcoString, EcoString)>>) -> Box<Self> {
Box::new(Self {
context: value.into(),
..self
})
}
pub fn attach_docs_url(self, value: impl Into<EcoString>) -> Box<Self> {
Box::new(Self {
docs_url: Some(value.into()),
..self
})
}
pub fn attach_error_code(self, value: impl Into<EcoString>) -> Box<Self> {
Box::new(Self {
error_code: Some(value.into()),
..self
})
}
pub fn attach_new_issue_url(self, value: impl Into<EcoString>) -> Box<Self> {
Box::new(Self {
new_issue_url: Some(value.into()),
..self
})
}
pub fn attach_existing_issue_url(mut self, value: impl Into<EcoString>) -> Box<Self> {
self.existing_issue_urls.push(value.into());
Box::new(self)
}
pub fn attach_severity(self, value: impl Into<Severity>) -> Box<Self> {
Box::new(Self {
severity: Some(value.into()),
..self
})
}
pub fn attach_status(self, value: impl Into<Status>) -> Box<Self> {
Box::new(Self {
status: Some(value.into()),
..self
})
}
pub fn overwrite_backtrace(self, value: impl Into<Backtrace>) -> Box<Self> {
Box::new(Self {
backtrace: Some(value.into()),
..self
})
}
}