iris_chat_protocol/
storage.rs1use super::SharedConnection;
2use nostr_double_ratchet_runtime::{Error as NdrError, Result as NdrResult, StorageAdapter};
3
4pub 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}