1use std::io::{Cursor, Write};
4use std::path::{Path, PathBuf};
5use std::{fs, io};
6
7use tracing::{debug, error, warn};
8
9use burble::le::Addr;
10use burble::{gatt, smp};
11
12#[derive(Clone, Debug)]
14pub struct KeyStore(Dir);
15
16impl KeyStore {
17 const NAME: &'static str = "keys";
18
19 #[inline(always)]
22 #[must_use]
23 pub fn open(root: impl AsRef<Path>) -> Self {
24 Self(Dir::open(root, Self::NAME))
25 }
26
27 #[inline(always)]
34 #[must_use]
35 pub fn per_user(app: impl AsRef<Path>) -> Self {
36 Self(Dir::per_user(app, Self::NAME))
37 }
38}
39
40impl burble::PeerStore for KeyStore {
41 type Value = smp::Keys;
42
43 #[inline(always)]
44 fn save(&self, peer: Addr, v: &Self::Value) -> bool {
45 self.0.save(peer, v)
46 }
47
48 #[inline(always)]
49 fn load(&self, peer: Addr) -> Option<Self::Value> {
50 self.0.load(peer)
51 }
52
53 #[inline(always)]
54 fn remove(&self, peer: Addr) {
55 self.0.remove(peer);
56 }
57
58 #[inline(always)]
59 fn clear(&self) {
60 self.0.clear();
61 }
62}
63
64#[derive(Clone, Debug)]
66pub struct GattServerStore(Dir);
67
68impl GattServerStore {
69 const NAME: &'static str = "gatts";
70
71 #[inline(always)]
74 #[must_use]
75 pub fn open(root: impl AsRef<Path>) -> Self {
76 Self(Dir::open(root, Self::NAME))
77 }
78
79 #[inline(always)]
86 #[must_use]
87 pub fn per_user(app: impl AsRef<Path>) -> Self {
88 Self(Dir::per_user(app, Self::NAME))
89 }
90}
91
92impl burble::PeerStore for GattServerStore {
93 type Value = gatt::Cache;
94
95 #[inline(always)]
96 fn save(&self, peer: Addr, v: &Self::Value) -> bool {
97 self.0.save(peer, v)
98 }
99
100 #[inline(always)]
101 fn load(&self, peer: Addr) -> Option<Self::Value> {
102 self.0.load(peer)
103 }
104
105 #[inline(always)]
106 fn remove(&self, peer: Addr) {
107 self.0.remove(peer);
108 }
109
110 #[inline(always)]
111 fn clear(&self) {
112 self.0.clear();
113 }
114}
115
116#[derive(Clone, Debug)]
118#[repr(transparent)]
119struct Dir(PathBuf);
120
121impl Dir {
122 const FILE_NAME_FMT: &'static str = "P-001122334455";
123
124 #[inline(always)]
126 #[must_use]
127 fn open(root: impl AsRef<Path>, name: impl AsRef<Path>) -> Self {
128 Self(root.as_ref().join(name))
129 }
130
131 #[must_use]
138 fn per_user(app: impl AsRef<Path>, name: impl AsRef<Path>) -> Self {
139 let dir = dirs::data_local_dir()
140 .expect("user directory not available")
141 .join(app.as_ref())
142 .join(name);
143 Self(dir)
144 }
145
146 fn save(&self, peer: Addr, v: &impl serde::ser::Serialize) -> bool {
148 let s = serde_json::to_string_pretty(v).expect("failed to serialize peer data");
149 if let Err(e) = fs::create_dir_all(&self.0) {
150 warn!(
151 "Failed to create database directory: {} ({e})",
152 self.0.display()
153 );
154 }
155 let path = self.path(peer);
156 match fs::File::create(&path)
158 .and_then(|mut f| f.write_all(s.as_bytes()).and_then(|_| f.sync_data()))
159 {
160 Ok(_) => {
161 debug!("Wrote: {}", path.display());
162 true
163 }
164 Err(e) => {
165 error!("Failed to write: {} ({e})", path.display());
166 false
167 }
168 }
169 }
170
171 fn load<T: serde::de::DeserializeOwned>(&self, peer: Addr) -> Option<T> {
173 let path = self.path(peer);
174 let s = match fs::read_to_string(&path) {
175 Ok(s) => s,
176 Err(e) if matches!(e.kind(), io::ErrorKind::NotFound) => return None,
177 Err(e) => {
178 error!("Failed to read: {} ({e})", path.display());
179 return None;
180 }
181 };
182 serde_json::from_str(&s)
183 .map_err(|e| {
184 error!("Invalid file contents: {} ({e})", path.display());
185 Err::<T, ()>(())
186 })
187 .ok()
188 }
189
190 fn remove(&self, peer: Addr) {
192 let path = self.path(peer);
193 match fs::remove_file(&path) {
194 Ok(_) => {}
195 Err(e) if matches!(e.kind(), io::ErrorKind::NotFound) => {}
196 Err(e) => error!("Failed to remove: {} ({e})", path.display()),
197 }
198 }
199
200 fn clear(&self) {
202 match fs::remove_dir_all(&self.0) {
203 Ok(_) => {}
204 Err(e) if matches!(e.kind(), io::ErrorKind::NotFound) => {}
205 Err(e) => error!("Failed to remove: {} ({e})", self.0.display()),
206 }
207 }
208
209 fn path(&self, peer: Addr) -> PathBuf {
211 let (raw, typ) = match peer {
212 Addr::Public(ref raw) => (raw.as_le_bytes(), 'P'),
213 Addr::Random(ref raw) => (raw.as_le_bytes(), 'R'),
214 };
215 let mut buf = Cursor::new([0_u8; Self::FILE_NAME_FMT.len()]);
216 write!(
217 buf,
218 "{typ}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
219 raw[5], raw[4], raw[3], raw[2], raw[1], raw[0]
220 )
221 .expect("key file name overflow");
222 (self.0).join(unsafe { std::str::from_utf8_unchecked(buf.get_ref()) })
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use tempfile::Builder;
230
231 use burble::le::RawAddr;
232 use burble::PeerStore;
233
234 use super::*;
235
236 #[test]
237 fn save_load() {
238 const PEER: Addr =
239 Addr::Public(RawAddr::from_le_bytes([0x55, 0x44, 0x33, 0x22, 0x11, 0x00]));
240 let tmp = (Builder::new().prefix(concat!("burble-test-")).tempdir()).unwrap();
241 let db = KeyStore(Dir(tmp.path().to_path_buf()));
242 let keys = smp::Keys::test();
243 assert!(db.save(PEER, &keys));
244 assert!(tmp.path().join(Dir::FILE_NAME_FMT).exists());
245 assert_eq!(db.load(PEER).unwrap(), keys);
246 }
247}