use std::path::Path;
use parking_lot::Mutex;
use rusqlite::{params, Connection, OpenFlags};
use zeroize::Zeroizing;
use crate::{Error, Result};
const SCHEMA_USER_VERSION: i64 = 1;
const BLOB_ROW_ID: i64 = 1;
#[derive(Debug)]
pub(crate) struct SqliteFile {
conn: Mutex<Connection>,
}
impl SqliteFile {
pub(crate) fn open(path: &Path, encryption_key: Option<&Zeroizing<[u8; 32]>>) -> Result<Self> {
let conn = Connection::open_with_flags(
path,
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
)
.map_err(|e| Error::Io(format!("open {}: {e}", path.display())))?;
if let Some(key) = encryption_key {
let hex_key: String = key.iter().map(|b| format!("{:02x}", b)).collect();
conn.execute_batch(&format!("PRAGMA key = \"x'{hex_key}'\";"))
.map_err(|e| Error::Io(format!("pragma key: {e}")))?;
if let Err(e) = conn.execute_batch("SELECT count(*) FROM sqlite_master;") {
let msg = format!("{e}");
if msg.contains("file is not a database") || msg.contains("file is encrypted") {
return Err(Error::EncryptionKeyMismatch);
}
return Err(Error::Io(format!("verify key: {e}")));
}
}
conn.execute_batch(
r#"
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = OFF;
"#,
)
.map_err(|e| Error::Io(format!("pragma init: {e}")))?;
let v: i64 = conn
.query_row("PRAGMA user_version", [], |r| r.get(0))
.map_err(|e| Error::Io(format!("user_version: {e}")))?;
match v {
0 => {
conn.execute_batch(&format!(
r#"
CREATE TABLE IF NOT EXISTS mls_blob (
id INTEGER PRIMARY KEY CHECK(id = {BLOB_ROW_ID}),
value BLOB NOT NULL
);
PRAGMA user_version = {SCHEMA_USER_VERSION};
"#
))
.map_err(|e| Error::Io(format!("migrate v0->v1: {e}")))?;
}
v if v == SCHEMA_USER_VERSION => { }
other => return Err(Error::UnsupportedSchema(other)),
}
Ok(Self {
conn: Mutex::new(conn),
})
}
pub(crate) fn read_blob(&self) -> Result<Option<Vec<u8>>> {
let conn = self.conn.lock();
let mut stmt = conn
.prepare_cached("SELECT value FROM mls_blob WHERE id = ?")
.map_err(|e| Error::Io(format!("prepare read_blob: {e}")))?;
let row: Option<Vec<u8>> = stmt
.query_row(params![BLOB_ROW_ID], |r| r.get::<_, Vec<u8>>(0))
.map(Some)
.or_else(|e| match e {
rusqlite::Error::QueryReturnedNoRows => Ok(None),
other => Err(other),
})
.map_err(|e| Error::Io(format!("read_blob: {e}")))?;
Ok(row)
}
pub(crate) fn write_blob(&self, value: &[u8]) -> Result<()> {
let conn = self.conn.lock();
conn.execute(
"INSERT INTO mls_blob (id, value) VALUES (?, ?) \
ON CONFLICT(id) DO UPDATE SET value = excluded.value",
params![BLOB_ROW_ID, value],
)
.map_err(|e| Error::Io(format!("write_blob: {e}")))?;
Ok(())
}
}