diff --git a/ftl/core/errors.ftl b/ftl/core/errors.ftl index 8f122dfde..5b74899e4 100644 --- a/ftl/core/errors.ftl +++ b/ftl/core/errors.ftl @@ -11,6 +11,7 @@ errors-multiple-notetypes-selected = Please select notes from only one notetype. errors-please-check-database = Please use the Check Database action, then try again. errors-please-check-media = Please use the Check Media action, then try again. errors-collection-too-new = This collection requires a newer version of Anki to open. +errors-invalid-ids = This deck contains timestamps in the future. Please contact the deck author and ask them to fix the issue. ## Card Rendering diff --git a/rslib/src/backend/error.rs b/rslib/src/backend/error.rs index 85ed1ed90..7eba97b34 100644 --- a/rslib/src/backend/error.rs +++ b/rslib/src/backend/error.rs @@ -39,6 +39,7 @@ impl AnkiError { AnkiError::ImportError(_) => Kind::ImportError, AnkiError::FileIoError(_) => Kind::IoError, AnkiError::MediaCheckRequired => Kind::InvalidInput, + AnkiError::InvalidId => Kind::InvalidInput, }; pb::BackendError { diff --git a/rslib/src/error/mod.rs b/rslib/src/error/mod.rs index 92a8c9fd4..32c6f9afa 100644 --- a/rslib/src/error/mod.rs +++ b/rslib/src/error/mod.rs @@ -49,6 +49,7 @@ pub enum AnkiError { MediaCheckRequired, CustomStudyError(CustomStudyError), ImportError(ImportError), + InvalidId, } impl std::error::Error for AnkiError {} @@ -105,6 +106,7 @@ impl AnkiError { AnkiError::CustomStudyError(err) => err.localized_description(tr), AnkiError::ImportError(err) => err.localized_description(tr), AnkiError::Deleted => tr.browsing_row_deleted().into(), + AnkiError::InvalidId => tr.errors_invalid_ids().into(), AnkiError::IoError(_) | AnkiError::JsonError(_) | AnkiError::ProtoError(_) diff --git a/rslib/src/import_export/gather.rs b/rslib/src/import_export/gather.rs index e0428b0f3..b73de1bba 100644 --- a/rslib/src/import_export/gather.rs +++ b/rslib/src/import_export/gather.rs @@ -41,6 +41,7 @@ impl ExchangeData { self.cards = col.gather_cards()?; self.decks = col.gather_decks()?; self.notetypes = col.gather_notetypes()?; + self.check_ids()?; if with_scheduling { self.revlog = col.gather_revlog()?; @@ -114,6 +115,18 @@ impl ExchangeData { card.deck_id = deck_id; } } + + fn check_ids(&self) -> Result<()> { + let now = TimestampMillis::now().0; + self.cards + .iter() + .map(|card| card.id.0) + .chain(self.notes.iter().map(|note| note.id.0)) + .chain(self.revlog.iter().map(|entry| entry.id.0)) + .any(|timestamp| timestamp > now) + .then(|| Err(AnkiError::InvalidId)) + .unwrap_or(Ok(())) + } } fn gather_media_names_from_note( @@ -225,3 +238,36 @@ impl Collection { .collect() } } + +#[cfg(test)] +mod test { + use super::*; + use crate::{collection::open_test_collection, search::SearchNode}; + + #[test] + fn should_gather_valid_notes() { + let mut data = ExchangeData::default(); + let mut col = open_test_collection(); + + let note = col.add_new_note("Basic"); + data.gather_data(&mut col, SearchNode::WholeCollection, true) + .unwrap(); + + assert_eq!(data.notes, [note]); + } + + #[test] + fn should_err_if_note_has_invalid_id() { + let mut data = ExchangeData::default(); + let mut col = open_test_collection(); + let now_micros = TimestampMillis::now().0 * 1000; + + let mut note = col.add_new_note("Basic"); + note.id = NoteId(now_micros); + col.add_note_only_with_id_undoable(&mut note).unwrap(); + + assert!(data + .gather_data(&mut col, SearchNode::WholeCollection, true) + .is_err()); + } +}