Skip to main content

iris_chat_protocol/
storage.rs

1use super::SharedConnection;
2use nostr_double_ratchet_runtime::{Error as NdrError, Result as NdrResult, StorageAdapter};
3
4/// SQLite-backed implementation of `nostr_double_ratchet::StorageAdapter`.
5/// Keys are namespaced by (owner_pubkey_hex, device_pubkey_hex) so a
6/// single database serves multiple owner accounts and devices without
7/// keyspace collisions — matching the per-(owner, device) directory
8/// scoping the previous file-backed adapter used.
9pub struct SqliteStorageAdapter {
10    conn: SharedConnection,
11    owner_pubkey_hex: String,
12    device_pubkey_hex: String,
13}
14
15impl SqliteStorageAdapter {
16    pub fn new(
17        conn: SharedConnection,
18        owner_pubkey_hex: String,
19        device_pubkey_hex: String,
20    ) -> Self {
21        Self {
22            conn,
23            owner_pubkey_hex,
24            device_pubkey_hex,
25        }
26    }
27
28    fn map_err<E: std::fmt::Display>(error: E) -> NdrError {
29        NdrError::Storage(error.to_string())
30    }
31}
32
33impl StorageAdapter for SqliteStorageAdapter {
34    fn get(&self, key: &str) -> NdrResult<Option<String>> {
35        let conn = self
36            .conn
37            .lock()
38            .map_err(|_| NdrError::Storage("ndr_kv connection mutex poisoned".to_string()))?;
39        conn.query_row(
40            "SELECT value FROM ndr_kv WHERE owner_pubkey_hex = ?1 AND device_pubkey_hex = ?2 AND key = ?3",
41            (&self.owner_pubkey_hex, &self.device_pubkey_hex, key),
42            |row| row.get::<_, String>(0),
43        )
44        .map(Some)
45        .or_else(|err| match err {
46            rusqlite::Error::QueryReturnedNoRows => Ok(None),
47            other => Err(Self::map_err(other)),
48        })
49    }
50
51    fn put(&self, key: &str, value: String) -> NdrResult<()> {
52        let conn = self
53            .conn
54            .lock()
55            .map_err(|_| NdrError::Storage("ndr_kv connection mutex poisoned".to_string()))?;
56        conn.execute(
57            "INSERT INTO ndr_kv (owner_pubkey_hex, device_pubkey_hex, key, value)
58             VALUES (?1, ?2, ?3, ?4)
59             ON CONFLICT(owner_pubkey_hex, device_pubkey_hex, key) DO UPDATE SET value = excluded.value",
60            (&self.owner_pubkey_hex, &self.device_pubkey_hex, key, &value),
61        )
62        .map_err(Self::map_err)?;
63        Ok(())
64    }
65
66    fn del(&self, key: &str) -> NdrResult<()> {
67        let conn = self
68            .conn
69            .lock()
70            .map_err(|_| NdrError::Storage("ndr_kv connection mutex poisoned".to_string()))?;
71        conn.execute(
72            "DELETE FROM ndr_kv WHERE owner_pubkey_hex = ?1 AND device_pubkey_hex = ?2 AND key = ?3",
73            (&self.owner_pubkey_hex, &self.device_pubkey_hex, key),
74        )
75        .map_err(Self::map_err)?;
76        Ok(())
77    }
78
79    fn list(&self, prefix: &str) -> NdrResult<Vec<String>> {
80        let conn = self
81            .conn
82            .lock()
83            .map_err(|_| NdrError::Storage("ndr_kv connection mutex poisoned".to_string()))?;
84        let mut stmt = conn
85            .prepare(
86                "SELECT key FROM ndr_kv
87                 WHERE owner_pubkey_hex = ?1 AND device_pubkey_hex = ?2 AND key LIKE ?3 ESCAPE '\\'",
88            )
89            .map_err(Self::map_err)?;
90        let pattern = format!("{}%", escape_like(prefix));
91        let rows = stmt
92            .query_map(
93                (&self.owner_pubkey_hex, &self.device_pubkey_hex, &pattern),
94                |row| row.get::<_, String>(0),
95            )
96            .map_err(Self::map_err)?;
97        let mut keys = Vec::new();
98        for row in rows {
99            keys.push(row.map_err(Self::map_err)?);
100        }
101        Ok(keys)
102    }
103}
104
105fn escape_like(input: &str) -> String {
106    let mut out = String::with_capacity(input.len());
107    for ch in input.chars() {
108        match ch {
109            '\\' | '%' | '_' => {
110                out.push('\\');
111                out.push(ch);
112            }
113            other => out.push(other),
114        }
115    }
116    out
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use std::sync::{Arc, Mutex};
123
124    fn fresh_connection() -> SharedConnection {
125        let conn = rusqlite::Connection::open_in_memory().unwrap();
126        conn.execute_batch(
127            "CREATE TABLE ndr_kv (
128                owner_pubkey_hex TEXT NOT NULL,
129                device_pubkey_hex TEXT NOT NULL,
130                key TEXT NOT NULL,
131                value TEXT NOT NULL,
132                PRIMARY KEY (owner_pubkey_hex, device_pubkey_hex, key)
133            );",
134        )
135        .unwrap();
136        Arc::new(Mutex::new(conn))
137    }
138
139    fn fresh_adapter() -> SqliteStorageAdapter {
140        SqliteStorageAdapter::new(
141            fresh_connection(),
142            "owner".to_string(),
143            "device".to_string(),
144        )
145    }
146
147    #[test]
148    fn put_get_del_round_trip() {
149        let adapter = fresh_adapter();
150        assert!(adapter.get("k").unwrap().is_none());
151        adapter.put("k", "v".to_string()).unwrap();
152        assert_eq!(adapter.get("k").unwrap(), Some("v".to_string()));
153        adapter.put("k", "v2".to_string()).unwrap();
154        assert_eq!(adapter.get("k").unwrap(), Some("v2".to_string()));
155        adapter.del("k").unwrap();
156        assert!(adapter.get("k").unwrap().is_none());
157    }
158
159    #[test]
160    fn list_returns_only_matching_prefix() {
161        let adapter = fresh_adapter();
162        adapter.put("user/alice", "1".to_string()).unwrap();
163        adapter.put("user/bob", "2".to_string()).unwrap();
164        adapter.put("invite/charlie", "3".to_string()).unwrap();
165        let mut keys = adapter.list("user/").unwrap();
166        keys.sort();
167        assert_eq!(keys, vec!["user/alice".to_string(), "user/bob".to_string()]);
168    }
169
170    #[test]
171    fn keys_are_isolated_per_owner_device() {
172        let conn = fresh_connection();
173        let alice = SqliteStorageAdapter::new(conn.clone(), "owner_a".into(), "device_a".into());
174        let bob = SqliteStorageAdapter::new(conn, "owner_b".into(), "device_b".into());
175        alice.put("shared-key", "alice".to_string()).unwrap();
176        bob.put("shared-key", "bob".to_string()).unwrap();
177        assert_eq!(alice.get("shared-key").unwrap(), Some("alice".to_string()));
178        assert_eq!(bob.get("shared-key").unwrap(), Some("bob".to_string()));
179    }
180}