// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html mod db; mod filtered; mod invalid_input; pub(crate) mod network; mod not_found; mod search; #[cfg(windows)] pub mod windows; use anki_i18n::I18n; use anki_io::FileIoError; use anki_io::FileOp; pub use db::DbError; pub use db::DbErrorKind; pub use filtered::CustomStudyError; pub use filtered::FilteredDeckError; pub use network::NetworkError; pub use network::NetworkErrorKind; pub use network::SyncError; pub use network::SyncErrorKind; pub use search::ParseError; pub use search::SearchErrorKind; use snafu::Snafu; pub use self::invalid_input::InvalidInputError; pub use self::invalid_input::OrInvalid; pub use self::not_found::NotFoundError; pub use self::not_found::OrNotFound; use crate::import_export::ImportError; use crate::links::HelpPage; pub type Result = std::result::Result; #[derive(Debug, PartialEq, Snafu)] pub enum AnkiError { #[snafu(context(false))] InvalidInput { source: InvalidInputError, }, TemplateError { info: String, }, #[snafu(context(false))] CardTypeError { source: CardTypeError, }, #[snafu(context(false))] FileIoError { source: FileIoError, }, #[snafu(context(false))] DbError { source: DbError, }, #[snafu(context(false))] NetworkError { source: NetworkError, }, #[snafu(context(false))] SyncError { source: SyncError, }, JsonError { info: String, }, ProtoError { info: String, }, ParseNumError, Interrupted, CollectionNotOpen, CollectionAlreadyOpen, #[snafu(context(false))] NotFound { source: NotFoundError, }, /// Indicates an absent card or note, but (unlike [AnkiError::NotFound]) in /// a non-critical context like the browser table, where deleted ids are /// deliberately not removed. Deleted, Existing, #[snafu(context(false))] FilteredDeckError { source: FilteredDeckError, }, #[snafu(context(false))] SearchError { source: SearchErrorKind, }, InvalidRegex { info: String, }, UndoEmpty, MultipleNotetypesSelected, DatabaseCheckRequired, MediaCheckRequired, #[snafu(context(false))] CustomStudyError { source: CustomStudyError, }, #[snafu(context(false))] ImportError { source: ImportError, }, InvalidId, #[cfg(windows)] #[snafu(context(false))] WindowsError { source: windows::WindowsError, }, InvalidMethodIndex, InvalidServiceIndex, FsrsWeightsInvalid, /// Returned by fsrs-rs; may happen even if 400+ reviews FsrsInsufficientData, /// Generated by our backend if count < 400 FsrsInsufficientReviews { count: usize, }, FsrsUnableToDetermineDesiredRetention, SchedulerUpgradeRequired, InvalidCertificateFormat, } // error helpers impl AnkiError { pub fn message(&self, tr: &I18n) -> String { match self { AnkiError::SyncError { source } => source.message(tr), AnkiError::NetworkError { source } => source.message(tr), AnkiError::TemplateError { info: source } => { // already localized source.into() } AnkiError::CardTypeError { source } => { let header = tr.card_templates_invalid_template_number(source.ordinal + 1, &source.notetype); let details = match &source.source { CardTypeErrorDetails::TemplateParseError => tr.card_templates_see_preview(), CardTypeErrorDetails::NoSuchField { field } => { tr.card_templates_field_not_found(field) } CardTypeErrorDetails::NoFrontField => tr.card_templates_no_front_field(), CardTypeErrorDetails::Duplicate { index } => { tr.card_templates_identical_front(index + 1) } CardTypeErrorDetails::MissingCloze => tr.card_templates_missing_cloze(), CardTypeErrorDetails::ExtraneousCloze => tr.card_templates_extraneous_cloze(), }; format!("{}
{}", header, details) } AnkiError::DbError { source } => source.message(tr), AnkiError::SearchError { source } => source.message(tr), AnkiError::ParseNumError => tr.errors_parse_number_fail().into(), AnkiError::FilteredDeckError { source } => source.message(tr), AnkiError::InvalidRegex { info: source } => format!("
{}
", source), AnkiError::MultipleNotetypesSelected => tr.errors_multiple_notetypes_selected().into(), AnkiError::DatabaseCheckRequired => tr.errors_please_check_database().into(), AnkiError::MediaCheckRequired => tr.errors_please_check_media().into(), AnkiError::CustomStudyError { source } => source.message(tr), AnkiError::ImportError { source } => source.message(tr), AnkiError::Deleted => tr.browsing_row_deleted().into(), AnkiError::InvalidId => tr.errors_please_check_database().into(), AnkiError::JsonError { .. } | AnkiError::ProtoError { .. } | AnkiError::Interrupted | AnkiError::CollectionNotOpen | AnkiError::CollectionAlreadyOpen | AnkiError::Existing | AnkiError::InvalidServiceIndex | AnkiError::InvalidMethodIndex | AnkiError::UndoEmpty | AnkiError::InvalidCertificateFormat => format!("{:?}", self), AnkiError::FileIoError { source } => source.message(), AnkiError::InvalidInput { source } => source.message(), AnkiError::NotFound { source } => source.message(tr), AnkiError::FsrsInsufficientData => tr.deck_config_not_enough_history().into(), AnkiError::FsrsInsufficientReviews { count } => { tr.deck_config_must_have_400_reviews(*count).into() } AnkiError::FsrsWeightsInvalid => tr.deck_config_invalid_weights().into(), AnkiError::SchedulerUpgradeRequired => { tr.scheduling_update_required().replace("V2", "v3") } #[cfg(windows)] AnkiError::WindowsError { source } => format!("{source:?}"), AnkiError::FsrsUnableToDetermineDesiredRetention => tr .deck_config_unable_to_determine_desired_retention() .into(), } } pub fn help_page(&self) -> Option { match self { Self::CardTypeError { source: CardTypeError { source, .. }, } => Some(match source { CardTypeErrorDetails::TemplateParseError => HelpPage::CardTypeTemplateError, CardTypeErrorDetails::NoSuchField { field: _ } => HelpPage::CardTypeTemplateError, CardTypeErrorDetails::Duplicate { .. } => HelpPage::CardTypeDuplicate, CardTypeErrorDetails::NoFrontField => HelpPage::CardTypeNoFrontField, CardTypeErrorDetails::MissingCloze => HelpPage::CardTypeMissingCloze, CardTypeErrorDetails::ExtraneousCloze => HelpPage::CardTypeExtraneousCloze, }), _ => None, } } pub fn context(&self) -> String { match self { Self::InvalidInput { source } => source.context(), Self::NotFound { source } => source.context(), _ => String::new(), } } pub fn backtrace(&self) -> String { match self { Self::InvalidInput { source } => { if let Some(bt) = snafu::ErrorCompat::backtrace(source) { return format!("{bt}"); } } Self::NotFound { source } => { if let Some(bt) = snafu::ErrorCompat::backtrace(source) { return format!("{bt}"); } } _ => (), } String::new() } } #[derive(Debug, PartialEq, Eq)] pub enum TemplateError { NoClosingBrackets(String), ConditionalNotClosed(String), ConditionalNotOpen { closed: String, currently_open: Option, }, FieldNotFound { filters: String, field: String, }, NoSuchConditional(String), } impl From for AnkiError { fn from(err: serde_json::Error) -> Self { AnkiError::JsonError { info: err.to_string(), } } } impl From for AnkiError { fn from(err: prost::EncodeError) -> Self { AnkiError::ProtoError { info: err.to_string(), } } } impl From for AnkiError { fn from(err: prost::DecodeError) -> Self { AnkiError::ProtoError { info: err.to_string(), } } } impl From for AnkiError { fn from(e: tempfile::PathPersistError) -> Self { FileIoError::from(e).into() } } impl From for AnkiError { fn from(e: tempfile::PersistError) -> Self { FileIoError::from(e).into() } } impl From for AnkiError { fn from(err: regex::Error) -> Self { AnkiError::InvalidRegex { info: err.to_string(), } } } // stopgap; implicit mapping should be phased out in favor of manual // context attachment impl From for AnkiError { fn from(source: std::io::Error) -> Self { FileIoError { path: std::path::PathBuf::new(), op: FileOp::Unknown, source, } .into() } } #[derive(Debug, PartialEq, Eq, Snafu)] #[snafu(visibility(pub))] pub struct CardTypeError { pub notetype: String, pub ordinal: usize, pub source: CardTypeErrorDetails, } #[derive(Debug, PartialEq, Eq, Snafu)] #[snafu(visibility(pub))] pub enum CardTypeErrorDetails { TemplateParseError, Duplicate { index: usize }, NoFrontField, NoSuchField { field: String }, MissingCloze, ExtraneousCloze, }