diff options
Diffstat (limited to 'src/storage.rs')
| -rw-r--r-- | src/storage.rs | 65 |
1 files changed, 55 insertions, 10 deletions
diff --git a/src/storage.rs b/src/storage.rs index cb7fcd9..93cb98c 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,4 +1,4 @@ -use crate::model::{Definitions, Entry}; +use crate::model::{Definitions, Entry, EntryStatus}; use anyhow::{Context, Result, ensure}; use rusqlite::{Connection, OptionalExtension, params}; use std::{ @@ -70,7 +70,30 @@ impl Storage { ); crate::subscription::subscription_url(&subscription.url)?; } + #[derive(serde::Deserialize)] + #[serde(deny_unknown_fields)] + struct GuardInput { + version: u32, + enabled: bool, + } + let guard_path = config_dir.join("guard.toml"); + let guard_enabled = if guard_path.exists() { + let guard: GuardInput = toml::from_str(&fs::read_to_string(&guard_path)?) + .context("Corrupt guard state; use explicit recover --disable")?; + ensure!( + guard.version == 1, + "Unsupported guard state; use explicit recover --disable" + ); + guard.enabled + } else { + false + }; let path = data_dir.join("state.sqlite3"); + let database_existed = path.exists(); + ensure!( + database_existed || !guard_enabled, + "Durable state is missing while enabled protection is requested; use explicit recover --disable" + ); if path.exists() { ensure!( fs::symlink_metadata(&path)?.is_file(), @@ -84,6 +107,10 @@ impl Storage { let version: u32 = db.query_row("PRAGMA user_version", [], |r| r.get(0))?; ensure!(version <= 1, "Unsupported database version"); if version == 0 { + ensure!( + !database_existed, + "Existing database has no state schema; use explicit recover --disable" + ); db.execute_batch("BEGIN IMMEDIATE; CREATE TABLE entries (subscription_id TEXT NOT NULL, ordinal INTEGER NOT NULL, id TEXT PRIMARY KEY, data TEXT NOT NULL); CREATE TABLE state (singleton INTEGER PRIMARY KEY CHECK(singleton=1), enabled INTEGER NOT NULL CHECK(enabled IN (0,1)), selected TEXT, generation INTEGER NOT NULL); @@ -156,13 +183,7 @@ impl Storage { let values = query .query_map([subscription_id], |r| r.get::<_, String>(0))? .collect::<rusqlite::Result<Vec<_>>>()?; - values - .iter() - .map(|v| { - serde_json::from_str(v) - .context("Corrupt saved configuration; explicit recovery is required") - }) - .collect() + values.iter().map(|v| decode_entry(v)).collect() } pub fn find_entry(&self, id: &str) -> Result<Option<Entry>> { @@ -171,7 +192,7 @@ impl Storage { r.get::<_, String>(0) }) .optional()? - .map(|v| serde_json::from_str(&v).context("Corrupt saved configuration")) + .map(|v| decode_entry(&v)) .transpose() } @@ -254,7 +275,17 @@ pub fn persist_recovery_disabled(data_dir: &Path) -> Result<()> { let db = Connection::open(&path)?; fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?; db.busy_timeout(std::time::Duration::from_secs(5))?; - db.execute_batch("PRAGMA synchronous=FULL; BEGIN EXCLUSIVE;")?; + db.execute_batch("PRAGMA synchronous=FULL;")?; + let version: u32 = db.query_row("PRAGMA user_version", [], |row| row.get(0))?; + ensure!( + version <= 1, + "Unknown database schema; repair it before explicit recovery" + ); + db.execute_batch("BEGIN EXCLUSIVE; + CREATE TABLE IF NOT EXISTS entries (subscription_id TEXT NOT NULL, ordinal INTEGER NOT NULL, id TEXT PRIMARY KEY, data TEXT NOT NULL); + CREATE TABLE IF NOT EXISTS state (singleton INTEGER PRIMARY KEY CHECK(singleton=1), enabled INTEGER NOT NULL CHECK(enabled IN (0,1)), selected TEXT, generation INTEGER NOT NULL); + INSERT OR IGNORE INTO state VALUES (1,0,NULL,0); + PRAGMA user_version=1;")?; db.execute( "UPDATE state SET enabled=0,generation=generation+1 WHERE singleton=1", [], @@ -262,3 +293,17 @@ pub fn persist_recovery_disabled(data_dir: &Path) -> Result<()> { db.execute_batch("COMMIT")?; Ok(()) } + +fn decode_entry(value: &str) -> Result<Entry> { + let entry: Entry = serde_json::from_str(value).context("Corrupt saved configuration")?; + ensure!( + entry.status == EntryStatus::Supported + && entry.connection.is_some() + && entry.explanation.is_none() + || entry.status != EntryStatus::Supported + && entry.connection.is_none() + && entry.explanation.is_some(), + "Saved entry has contradictory validation state; explicit recovery is required" + ); + Ok(entry) +} |