// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html /// At the moment, this is just basic note reading/updating functionality for /// the media DB check. use crate::err::{AnkiError, DBErrorKind, Result}; use crate::text::strip_html_preserving_image_filenames; use crate::time::i64_unix_secs; use crate::types::{ObjID, Timestamp, Usn}; use rusqlite::{params, Connection, Row, NO_PARAMS}; use serde_aux::field_attributes::deserialize_number_from_string; use serde_derive::Deserialize; use std::collections::HashMap; use std::convert::TryInto; #[derive(Debug)] pub(super) struct Note { pub id: ObjID, pub mid: ObjID, pub mtime_secs: Timestamp, pub usn: Usn, fields: Vec, } impl Note { pub fn fields(&self) -> &Vec { &self.fields } pub fn set_field(&mut self, idx: usize, text: impl Into) -> Result<()> { if idx >= self.fields.len() { return Err(AnkiError::invalid_input( "field idx out of range".to_string(), )); } self.fields[idx] = text.into(); Ok(()) } } pub(crate) fn field_checksum(text: &str) -> u32 { let text = strip_html_preserving_image_filenames(text); let digest = sha1::Sha1::from(text.as_ref()).digest().bytes(); u32::from_be_bytes(digest[..4].try_into().unwrap()) } #[derive(Deserialize, Debug)] pub(super) struct NoteType { #[serde(deserialize_with = "deserialize_number_from_string")] id: ObjID, #[serde(rename = "sortf")] sort_field_idx: u16, #[serde(rename = "latexsvg", default)] latex_svg: bool, } impl NoteType { pub fn latex_uses_svg(&self) -> bool { self.latex_svg } } pub(super) fn get_note_types(db: &Connection) -> Result> { let mut stmt = db.prepare("select models from col")?; let note_types = stmt .query_and_then(NO_PARAMS, |row| -> Result> { let v: HashMap = serde_json::from_str(row.get_raw(0).as_str()?)?; Ok(v) })? .next() .ok_or_else(|| AnkiError::DBError { info: "col table empty".to_string(), kind: DBErrorKind::MissingEntity, })??; Ok(note_types) } #[allow(dead_code)] fn get_note(db: &Connection, nid: ObjID) -> Result> { let mut stmt = db.prepare_cached("select id, mid, mod, usn, flds from notes where id=?")?; let note = stmt.query_and_then(params![nid], row_to_note)?.next(); note.transpose() } pub(super) fn for_every_note Result<()>>( db: &Connection, mut func: F, ) -> Result<()> { let mut stmt = db.prepare("select id, mid, mod, usn, flds from notes")?; for result in stmt.query_and_then(NO_PARAMS, |row| { let mut note = row_to_note(row)?; func(&mut note) })? { result?; } Ok(()) } fn row_to_note(row: &Row) -> Result { Ok(Note { id: row.get(0)?, mid: row.get(1)?, mtime_secs: row.get(2)?, usn: row.get(3)?, fields: row .get_raw(4) .as_str()? .split('\x1f') .map(|s| s.to_string()) .collect(), }) } pub(super) fn set_note(db: &Connection, note: &mut Note, note_type: &NoteType) -> Result<()> { note.mtime_secs = i64_unix_secs(); // hard-coded for now note.usn = -1; let csum = field_checksum(¬e.fields()[0]); let sort_field = strip_html_preserving_image_filenames( note.fields() .get(note_type.sort_field_idx as usize) .ok_or_else(|| AnkiError::DBError { info: "sort field out of range".to_string(), kind: DBErrorKind::MissingEntity, })?, ); let mut stmt = db.prepare_cached("update notes set mod=?,usn=?,flds=?,sfld=?,csum=? where id=?")?; stmt.execute(params![ note.mtime_secs, note.usn, note.fields().join("\x1f"), sort_field, csum, note.id, ])?; Ok(()) }