Skip to main content

layer_client/
session_backend.rs

1//! Pluggable session storage backend.
2//!
3//! The [`SessionBackend`] trait abstracts over session persistence so that
4//! callers can swap in an SQLite store, a custom binary file, an in-memory
5//! store, or anything else.
6//!
7//! Two built-in backends are provided:
8//! * [`BinaryFileBackend`] — the original binary file format (default).
9//! * [`SqliteBackend`] — SQLite (requires the `sqlite-session` Cargo feature).
10
11use std::io;
12use std::path::PathBuf;
13use crate::session::{DcEntry, PersistedSession};
14
15// ─── Trait ────────────────────────────────────────────────────────────────────
16
17/// An abstraction over where and how session data is persisted.
18pub trait SessionBackend: Send + Sync {
19    /// Persist the given session.
20    fn save(&self, session: &PersistedSession) -> io::Result<()>;
21
22    /// Load a previously persisted session, or return `None` if none exists.
23    fn load(&self) -> io::Result<Option<PersistedSession>>;
24
25    /// Remove the stored session (e.g. on sign-out).
26    fn delete(&self) -> io::Result<()>;
27
28    /// Human-readable name of this backend (for log messages).
29    fn name(&self) -> &str;
30}
31
32// ─── BinaryFileBackend ────────────────────────────────────────────────────────
33
34/// The default session backend — stores the session in a compact binary file.
35///
36/// This is the same format used by [`crate::session::PersistedSession`].
37pub struct BinaryFileBackend {
38    path: PathBuf,
39}
40
41impl BinaryFileBackend {
42    pub fn new(path: impl Into<PathBuf>) -> Self {
43        Self { path: path.into() }
44    }
45}
46
47impl SessionBackend for BinaryFileBackend {
48    fn save(&self, session: &PersistedSession) -> io::Result<()> {
49        session.save(&self.path)
50    }
51
52    fn load(&self) -> io::Result<Option<PersistedSession>> {
53        if !self.path.exists() {
54            return Ok(None);
55        }
56        PersistedSession::load(&self.path).map(Some)
57    }
58
59    fn delete(&self) -> io::Result<()> {
60        if self.path.exists() {
61            std::fs::remove_file(&self.path)?;
62        }
63        Ok(())
64    }
65
66    fn name(&self) -> &str { "binary-file" }
67}
68
69// ─── InMemoryBackend ─────────────────────────────────────────────────────────
70
71/// An ephemeral session backend that stores nothing on disk.
72///
73/// Useful for testing or for bots that should always start fresh.
74pub struct InMemoryBackend {
75    data: std::sync::Mutex<Option<PersistedSessionData>>,
76}
77
78#[derive(Clone)]
79struct PersistedSessionData {
80    home_dc_id: i32,
81    dcs:        Vec<DcEntry>,
82}
83
84impl InMemoryBackend {
85    pub fn new() -> Self {
86        Self { data: std::sync::Mutex::new(None) }
87    }
88}
89
90impl Default for InMemoryBackend {
91    fn default() -> Self { Self::new() }
92}
93
94impl SessionBackend for InMemoryBackend {
95    fn save(&self, session: &PersistedSession) -> io::Result<()> {
96        let mut lock = self.data.lock().unwrap();
97        *lock = Some(PersistedSessionData {
98            home_dc_id: session.home_dc_id,
99            dcs:        session.dcs.clone(),
100        });
101        Ok(())
102    }
103
104    fn load(&self) -> io::Result<Option<PersistedSession>> {
105        let lock = self.data.lock().unwrap();
106        Ok(lock.as_ref().map(|d| PersistedSession {
107            home_dc_id: d.home_dc_id,
108            dcs:        d.dcs.clone(),
109        }))
110    }
111
112    fn delete(&self) -> io::Result<()> {
113        let mut lock = self.data.lock().unwrap();
114        *lock = None;
115        Ok(())
116    }
117
118    fn name(&self) -> &str { "in-memory" }
119}
120
121// ─── SqliteBackend ────────────────────────────────────────────────────────────
122
123#[cfg(feature = "sqlite-session")]
124pub use sqlite_backend::SqliteBackend;
125
126#[cfg(feature = "sqlite-session")]
127mod sqlite_backend {
128    use super::*;
129    use rusqlite::{Connection, params};
130
131    /// SQLite-backed session store.
132    ///
133    /// Creates two tables (`meta` and `dc_entries`) if they do not exist.
134    ///
135    /// Enable with the `sqlite-session` Cargo feature:
136    /// ```toml
137    /// [dependencies]
138    /// layer-client = { version = "*", features = ["sqlite-session"] }
139    /// ```
140    pub struct SqliteBackend {
141        path: PathBuf,
142    }
143
144    impl SqliteBackend {
145        pub fn new(path: impl Into<PathBuf>) -> io::Result<Self> {
146            let path = path.into();
147            // Open and initialise the schema immediately so errors surface early.
148            let conn = Connection::open(&path)
149                .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
150            conn.execute_batch(
151                "CREATE TABLE IF NOT EXISTS meta (
152                    key   TEXT PRIMARY KEY,
153                    value TEXT NOT NULL
154                );
155                CREATE TABLE IF NOT EXISTS dc_entries (
156                    dc_id       INTEGER PRIMARY KEY,
157                    addr        TEXT    NOT NULL,
158                    auth_key    BLOB,
159                    first_salt  INTEGER NOT NULL DEFAULT 0,
160                    time_offset INTEGER NOT NULL DEFAULT 0
161                );",
162            ).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
163            Ok(Self { path })
164        }
165    }
166
167    impl SessionBackend for SqliteBackend {
168        fn save(&self, session: &PersistedSession) -> io::Result<()> {
169            let conn = Connection::open(&self.path)
170                .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
171
172            conn.execute(
173                "INSERT OR REPLACE INTO meta (key, value) VALUES ('home_dc_id', ?1)",
174                params![session.home_dc_id.to_string()],
175            ).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
176
177            for dc in &session.dcs {
178                let key_blob: Option<Vec<u8>> = dc.auth_key.map(|k| k.to_vec());
179                conn.execute(
180                    "INSERT OR REPLACE INTO dc_entries
181                        (dc_id, addr, auth_key, first_salt, time_offset)
182                     VALUES (?1, ?2, ?3, ?4, ?5)",
183                    params![
184                        dc.dc_id,
185                        dc.addr,
186                        key_blob,
187                        dc.first_salt,
188                        dc.time_offset,
189                    ],
190                ).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
191            }
192            Ok(())
193        }
194
195        fn load(&self) -> io::Result<Option<PersistedSession>> {
196            if !self.path.exists() {
197                return Ok(None);
198            }
199            let conn = Connection::open(&self.path)
200                .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
201
202            let home_dc_id: Option<i32> = conn
203                .query_row(
204                    "SELECT value FROM meta WHERE key = 'home_dc_id'",
205                    [],
206                    |row| {
207                        let v: String = row.get(0)?;
208                        Ok(v.parse::<i32>().unwrap_or(2))
209                    },
210                )
211                .ok();
212
213            let home_dc_id = match home_dc_id {
214                Some(id) => id,
215                None => return Ok(None),
216            };
217
218            let mut stmt = conn
219                .prepare("SELECT dc_id, addr, auth_key, first_salt, time_offset FROM dc_entries")
220                .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
221
222            let dcs: Vec<DcEntry> = stmt
223                .query_map([], |row| {
224                    let dc_id:       i32         = row.get(0)?;
225                    let addr:        String       = row.get(1)?;
226                    let key_blob:    Option<Vec<u8>> = row.get(2)?;
227                    let first_salt:  i64          = row.get(3)?;
228                    let time_offset: i32          = row.get(4)?;
229                    let auth_key = key_blob.and_then(|k| {
230                        if k.len() == 256 {
231                            let mut arr = [0u8; 256];
232                            arr.copy_from_slice(&k);
233                            Some(arr)
234                        } else {
235                            None
236                        }
237                    });
238                    Ok(DcEntry { dc_id, addr, auth_key, first_salt, time_offset })
239                })
240                .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?
241                .filter_map(|r| r.ok())
242                .collect();
243
244            Ok(Some(PersistedSession { home_dc_id, dcs }))
245        }
246
247        fn delete(&self) -> io::Result<()> {
248            if self.path.exists() {
249                std::fs::remove_file(&self.path)?;
250            }
251            Ok(())
252        }
253
254        fn name(&self) -> &str { "sqlite" }
255    }
256}