use std::path::Path;
use rusqlite::{Connection, OptionalExtension};
use tokio::sync::Mutex;
use zeroize::Zeroize;
use crate::error::PlatformError;
use crate::traits::Storage;
pub struct AppleStorage {
conn: Mutex<Connection>,
}
impl AppleStorage {
pub fn open(dir: &Path, encryption_key: &[u8]) -> Result<Self, PlatformError> {
if encryption_key.len() != 32 {
return Err(PlatformError::StorageError(format!(
"encryption key must be exactly 32 bytes, got {}",
encryption_key.len()
)));
}
let db_path = dir.join("scp.db");
let conn = Connection::open(&db_path).map_err(|e| {
PlatformError::StorageError(format!(
"failed to open database at {}: {e}",
db_path.display()
))
})?;
let mut hex_key = hex::encode(encryption_key);
let mut pragma_sql = format!(
"PRAGMA key = \"x'{hex_key}'\";\
PRAGMA cipher_page_size = 4096;\
PRAGMA kdf_iter = 256000;\
PRAGMA cipher_hmac_algorithm = HMAC_SHA512;\
PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA512;"
);
hex_key.zeroize();
let result = conn.execute_batch(&pragma_sql);
pragma_sql.zeroize();
result.map_err(|e| {
PlatformError::StorageError(format!("failed to apply `SQLCipher` pragmas: {e}"))
})?;
conn.execute_batch("PRAGMA journal_mode = WAL;")
.map_err(|e| PlatformError::StorageError(format!("failed to set WAL mode: {e}")))?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS kv (\
key TEXT PRIMARY KEY,\
value BLOB NOT NULL\
) WITHOUT ROWID;",
)
.map_err(|e| PlatformError::StorageError(format!("failed to create kv table: {e}")))?;
Ok(Self {
conn: Mutex::new(conn),
})
}
}
fn prefix_successor(prefix: &str) -> Option<String> {
let mut bytes = prefix.as_bytes().to_vec();
while bytes.last() == Some(&0xFF) {
bytes.pop();
}
if bytes.is_empty() {
return None;
}
if let Some(last) = bytes.last_mut() {
*last += 1;
}
String::from_utf8(bytes).ok()
}
#[allow(
clippy::manual_async_fn,
clippy::significant_drop_tightening,
clippy::single_match_else
)]
impl Storage for AppleStorage {
fn store(
&self,
key: &str,
data: &[u8],
) -> impl Future<Output = Result<(), PlatformError>> + Send {
let key = key.to_owned();
let data = data.to_vec();
async move {
let conn = self.conn.lock().await;
conn.execute(
"INSERT OR REPLACE INTO kv (key, value) VALUES (?1, ?2)",
rusqlite::params![key, data],
)
.map_err(|e| PlatformError::StorageError(format!("store failed: {e}")))?;
Ok(())
}
}
fn retrieve(
&self,
key: &str,
) -> impl Future<Output = Result<Option<Vec<u8>>, PlatformError>> + Send {
let key = key.to_owned();
async move {
let conn = self.conn.lock().await;
let mut stmt = conn
.prepare_cached("SELECT value FROM kv WHERE key = ?1")
.map_err(|e| {
PlatformError::StorageError(format!("retrieve prepare failed: {e}"))
})?;
let result = stmt
.query_row(rusqlite::params![key], |row| row.get::<_, Vec<u8>>(0))
.optional()
.map_err(|e| PlatformError::StorageError(format!("retrieve failed: {e}")))?;
Ok(result)
}
}
fn delete(&self, key: &str) -> impl Future<Output = Result<(), PlatformError>> + Send {
let key = key.to_owned();
async move {
let conn = self.conn.lock().await;
conn.execute("DELETE FROM kv WHERE key = ?1", rusqlite::params![key])
.map_err(|e| PlatformError::StorageError(format!("delete failed: {e}")))?;
Ok(())
}
}
fn list_keys(
&self,
prefix: &str,
) -> impl Future<Output = Result<Vec<String>, PlatformError>> + Send {
let prefix = prefix.to_owned();
async move {
let conn = self.conn.lock().await;
let keys = match prefix_successor(&prefix) {
Some(upper) => {
let mut stmt = conn
.prepare_cached(
"SELECT key FROM kv WHERE key >= ?1 AND key < ?2 ORDER BY key",
)
.map_err(|e| {
PlatformError::StorageError(format!("list_keys prepare failed: {e}"))
})?;
let rows = stmt
.query_map(rusqlite::params![prefix, upper], |row| {
row.get::<_, String>(0)
})
.map_err(|e| {
PlatformError::StorageError(format!("list_keys query failed: {e}"))
})?;
rows.collect::<Result<Vec<_>, _>>().map_err(|e| {
PlatformError::StorageError(format!("list_keys row read failed: {e}"))
})?
}
None => {
let mut stmt = conn
.prepare_cached("SELECT key FROM kv WHERE key >= ?1 ORDER BY key")
.map_err(|e| {
PlatformError::StorageError(format!("list_keys prepare failed: {e}"))
})?;
let rows = stmt
.query_map(rusqlite::params![prefix], |row| row.get::<_, String>(0))
.map_err(|e| {
PlatformError::StorageError(format!("list_keys query failed: {e}"))
})?;
rows.collect::<Result<Vec<_>, _>>().map_err(|e| {
PlatformError::StorageError(format!("list_keys row read failed: {e}"))
})?
}
};
Ok(keys)
}
}
fn delete_prefix(
&self,
prefix: &str,
) -> impl Future<Output = Result<u64, PlatformError>> + Send {
let prefix = prefix.to_owned();
async move {
let conn = self.conn.lock().await;
let deleted = match prefix_successor(&prefix) {
Some(upper) => conn
.execute(
"DELETE FROM kv WHERE key >= ?1 AND key < ?2",
rusqlite::params![prefix, upper],
)
.map_err(|e| {
PlatformError::StorageError(format!("delete_prefix failed: {e}"))
})?,
None => conn
.execute("DELETE FROM kv WHERE key >= ?1", rusqlite::params![prefix])
.map_err(|e| {
PlatformError::StorageError(format!("delete_prefix failed: {e}"))
})?,
};
Ok(deleted as u64)
}
}
fn exists(&self, key: &str) -> impl Future<Output = Result<bool, PlatformError>> + Send {
let key = key.to_owned();
async move {
let conn = self.conn.lock().await;
let mut stmt = conn
.prepare_cached("SELECT 1 FROM kv WHERE key = ?1 LIMIT 1")
.map_err(|e| PlatformError::StorageError(format!("exists prepare failed: {e}")))?;
let found = stmt
.query_row(rusqlite::params![key], |_row| Ok(true))
.optional()
.map_err(|e| PlatformError::StorageError(format!("exists failed: {e}")))?;
Ok(found.unwrap_or(false))
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use std::path::PathBuf;
fn test_storage() -> (AppleStorage, PathBuf) {
let dir = tempfile::tempdir().unwrap().keep();
let key = [0x42u8; 32];
let storage = AppleStorage::open(&dir, &key).unwrap();
(storage, dir)
}
#[tokio::test]
async fn store_and_retrieve_roundtrip() {
let (storage, _dir) = test_storage();
storage.store("key1", b"value1").await.unwrap();
let result = storage.retrieve("key1").await.unwrap();
assert_eq!(result, Some(b"value1".to_vec()));
}
#[tokio::test]
async fn retrieve_nonexistent_returns_none() {
let (storage, _dir) = test_storage();
let result = storage.retrieve("missing").await.unwrap();
assert_eq!(result, None);
}
#[tokio::test]
async fn store_overwrites_existing_value() {
let (storage, _dir) = test_storage();
storage.store("key", b"first").await.unwrap();
storage.store("key", b"second").await.unwrap();
let result = storage.retrieve("key").await.unwrap();
assert_eq!(result, Some(b"second".to_vec()));
}
#[tokio::test]
async fn delete_removes_value() {
let (storage, _dir) = test_storage();
storage.store("key", b"value").await.unwrap();
storage.delete("key").await.unwrap();
let result = storage.retrieve("key").await.unwrap();
assert_eq!(result, None);
}
#[tokio::test]
async fn delete_nonexistent_is_noop() {
let (storage, _dir) = test_storage();
storage.delete("nonexistent").await.unwrap();
}
#[tokio::test]
async fn list_keys_returns_matching_prefix_in_sorted_order() {
let (storage, _dir) = test_storage();
storage.store("prefix/c", b"").await.unwrap();
storage.store("prefix/a", b"").await.unwrap();
storage.store("prefix/b", b"").await.unwrap();
storage.store("other/x", b"").await.unwrap();
let keys = storage.list_keys("prefix/").await.unwrap();
assert_eq!(keys, vec!["prefix/a", "prefix/b", "prefix/c"]);
}
#[tokio::test]
async fn list_keys_empty_prefix_returns_all_sorted() {
let (storage, _dir) = test_storage();
storage.store("b", b"").await.unwrap();
storage.store("a", b"").await.unwrap();
let keys = storage.list_keys("").await.unwrap();
assert_eq!(keys, vec!["a", "b"]);
}
#[tokio::test]
async fn list_keys_no_matches_returns_empty() {
let (storage, _dir) = test_storage();
storage.store("foo", b"").await.unwrap();
let keys = storage.list_keys("bar").await.unwrap();
assert!(keys.is_empty());
}
#[tokio::test]
async fn delete_prefix_removes_matching_keys_and_returns_count() {
let (storage, _dir) = test_storage();
storage.store("ctx/a", b"1").await.unwrap();
storage.store("ctx/b", b"2").await.unwrap();
storage.store("ctx/c", b"3").await.unwrap();
storage.store("other/d", b"4").await.unwrap();
let deleted = storage.delete_prefix("ctx/").await.unwrap();
assert_eq!(deleted, 3);
assert_eq!(storage.retrieve("ctx/a").await.unwrap(), None);
assert_eq!(storage.retrieve("ctx/b").await.unwrap(), None);
assert_eq!(storage.retrieve("ctx/c").await.unwrap(), None);
assert_eq!(
storage.retrieve("other/d").await.unwrap(),
Some(b"4".to_vec())
);
}
#[tokio::test]
async fn delete_prefix_no_matches_returns_zero() {
let (storage, _dir) = test_storage();
storage.store("foo", b"bar").await.unwrap();
let deleted = storage.delete_prefix("zzz").await.unwrap();
assert_eq!(deleted, 0);
}
#[tokio::test]
async fn exists_returns_true_for_stored_key() {
let (storage, _dir) = test_storage();
storage.store("key", b"value").await.unwrap();
assert!(storage.exists("key").await.unwrap());
}
#[tokio::test]
async fn exists_returns_false_for_missing_key() {
let (storage, _dir) = test_storage();
assert!(!storage.exists("missing").await.unwrap());
}
#[tokio::test]
async fn exists_returns_false_after_delete() {
let (storage, _dir) = test_storage();
storage.store("key", b"value").await.unwrap();
storage.delete("key").await.unwrap();
assert!(!storage.exists("key").await.unwrap());
}
#[tokio::test]
async fn store_empty_value_succeeds() {
let (storage, _dir) = test_storage();
storage.store("empty", b"").await.unwrap();
let result = storage.retrieve("empty").await.unwrap();
assert_eq!(result, Some(vec![]));
}
#[tokio::test]
async fn rejects_wrong_key_length() {
let dir = tempfile::tempdir().unwrap();
let short_key = [0u8; 16];
let result = AppleStorage::open(dir.path(), &short_key);
assert!(result.is_err());
}
#[tokio::test]
async fn reopen_persists_data() {
let dir = tempfile::tempdir().unwrap().keep();
let key = [0xABu8; 32];
{
let storage = AppleStorage::open(&dir, &key).unwrap();
storage.store("persist", b"across restarts").await.unwrap();
}
{
let storage = AppleStorage::open(&dir, &key).unwrap();
let result = storage.retrieve("persist").await.unwrap();
assert_eq!(result, Some(b"across restarts".to_vec()));
}
}
#[test]
fn prefix_successor_normal() {
assert_eq!(prefix_successor("abc"), Some("abd".to_owned()));
}
#[test]
fn prefix_successor_trailing_high_byte() {
assert_eq!(prefix_successor("ab~"), Some("ab\x7f".to_owned()));
}
#[test]
fn prefix_successor_empty() {
assert_eq!(prefix_successor(""), None);
}
#[test]
fn prefix_successor_single_char() {
assert_eq!(prefix_successor("a"), Some("b".to_owned()));
}
}