1use std::collections::HashMap;
9use std::fs;
10use std::io;
11use std::path::{Path, PathBuf};
12
13use rns_crypto::identity::Identity;
14use rns_crypto::OsRng;
15
16#[derive(Debug, Clone)]
18pub struct StoragePaths {
19 pub config_dir: PathBuf,
20 pub storage: PathBuf,
21 pub cache: PathBuf,
22 pub identities: PathBuf,
23 pub discovered_interfaces: PathBuf,
25}
26
27#[derive(Debug, Clone)]
29pub struct KnownDestination {
30 pub identity_hash: [u8; 16],
31 pub public_key: [u8; 64],
32 pub app_data: Option<Vec<u8>>,
33 pub hops: u8,
34 pub received_at: f64,
35 pub receiving_interface: u64,
36 pub was_used: bool,
37 pub last_used_at: Option<f64>,
38 pub retained: bool,
39}
40
41pub fn ensure_storage_dirs(config_dir: &Path) -> io::Result<StoragePaths> {
43 let storage = config_dir.join("storage");
44 let cache = config_dir.join("cache");
45 let identities = storage.join("identities");
46 let announces = cache.join("announces");
47 let discovered_interfaces = storage.join("discovery").join("interfaces");
48
49 fs::create_dir_all(&storage)?;
50 fs::create_dir_all(&cache)?;
51 fs::create_dir_all(&identities)?;
52 fs::create_dir_all(&announces)?;
53 fs::create_dir_all(&discovered_interfaces)?;
54
55 Ok(StoragePaths {
56 config_dir: config_dir.to_path_buf(),
57 storage,
58 cache,
59 identities,
60 discovered_interfaces,
61 })
62}
63
64pub fn save_identity(identity: &Identity, path: &Path) -> io::Result<()> {
66 let private_key = identity
67 .get_private_key()
68 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Identity has no private key"))?;
69 fs::write(path, &private_key)
70}
71
72pub fn load_identity(path: &Path) -> io::Result<Identity> {
74 let data = fs::read(path)?;
75 if data.len() != 64 {
76 return Err(io::Error::new(
77 io::ErrorKind::InvalidData,
78 format!("Identity file must be 64 bytes, got {}", data.len()),
79 ));
80 }
81 let mut key = [0u8; 64];
82 key.copy_from_slice(&data);
83 Ok(Identity::from_private_key(&key))
84}
85
86pub fn save_known_destinations(
93 destinations: &HashMap<[u8; 16], KnownDestination>,
94 path: &Path,
95) -> io::Result<()> {
96 use rns_core::msgpack::{self, Value};
97
98 let entries: Vec<(Value, Value)> = destinations
99 .iter()
100 .map(|(hash, dest)| {
101 let key = Value::Bin(hash.to_vec());
102 let app_data = match &dest.app_data {
103 Some(d) => Value::Bin(d.clone()),
104 None => Value::Nil,
105 };
106 let value = Value::Array(vec![
107 Value::UInt(dest.received_at as u64),
108 Value::Bin(dest.public_key.to_vec()),
109 app_data,
110 Value::Bin(dest.identity_hash.to_vec()),
111 Value::UInt(dest.hops as u64),
112 Value::UInt(dest.receiving_interface),
113 Value::Bool(dest.was_used),
114 match dest.last_used_at {
115 Some(last_used_at) => Value::UInt(last_used_at as u64),
116 None => Value::Nil,
117 },
118 Value::Bool(dest.retained),
119 ]);
120 (key, value)
121 })
122 .collect();
123
124 let packed = msgpack::pack(&Value::Map(entries));
125 fs::write(path, packed)
126}
127
128pub fn load_known_destinations(path: &Path) -> io::Result<HashMap<[u8; 16], KnownDestination>> {
130 use rns_core::msgpack;
131
132 let data = fs::read(path)?;
133 if data.is_empty() {
134 return Ok(HashMap::new());
135 }
136
137 let (value, _) = msgpack::unpack(&data)
138 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("msgpack error: {}", e)))?;
139
140 let map = value
141 .as_map()
142 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Expected msgpack map"))?;
143
144 let mut result = HashMap::new();
145
146 for (k, v) in map {
147 let hash_bytes = k
148 .as_bin()
149 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Expected bin key"))?;
150
151 if hash_bytes.len() != 16 {
152 continue; }
154
155 let mut dest_hash = [0u8; 16];
156 dest_hash.copy_from_slice(hash_bytes);
157
158 let arr = v
159 .as_array()
160 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Expected array value"))?;
161
162 if arr.len() < 3 {
163 continue;
164 }
165
166 let received_at = arr[0].as_uint().unwrap_or(0) as f64;
167
168 let pub_key_bytes = arr[1]
169 .as_bin()
170 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Expected bin public_key"))?;
171 if pub_key_bytes.len() != 64 {
172 continue;
173 }
174 let mut public_key = [0u8; 64];
175 public_key.copy_from_slice(pub_key_bytes);
176
177 let app_data = if arr.len() > 2 {
178 arr[2].as_bin().map(|b| b.to_vec())
179 } else {
180 None
181 };
182
183 let identity_hash = if arr.len() > 3 {
184 let hash_bytes = arr[3]
185 .as_bin()
186 .filter(|bytes| bytes.len() == 16)
187 .map(|bytes| {
188 let mut hash = [0u8; 16];
189 hash.copy_from_slice(bytes);
190 hash
191 });
192 hash_bytes.unwrap_or_else(|| {
193 let identity = Identity::from_public_key(&public_key);
194 *identity.hash()
195 })
196 } else {
197 let identity = Identity::from_public_key(&public_key);
198 *identity.hash()
199 };
200 let hops = arr.get(4).and_then(|value| value.as_uint()).unwrap_or(0) as u8;
201 let receiving_interface = arr.get(5).and_then(|value| value.as_uint()).unwrap_or(0);
202 let was_used = arr
203 .get(6)
204 .and_then(|value| value.as_bool())
205 .unwrap_or(false);
206 let last_used_at = arr
207 .get(7)
208 .and_then(|value| value.as_uint())
209 .map(|value| value as f64);
210 let retained = arr
211 .get(8)
212 .and_then(|value| value.as_bool())
213 .unwrap_or(false);
214
215 result.insert(
216 dest_hash,
217 KnownDestination {
218 identity_hash,
219 public_key,
220 app_data,
221 hops,
222 received_at,
223 receiving_interface,
224 was_used,
225 last_used_at,
226 retained,
227 },
228 );
229 }
230
231 Ok(result)
232}
233
234pub fn resolve_config_dir(explicit: Option<&Path>) -> PathBuf {
237 if let Some(p) = explicit {
238 p.to_path_buf()
239 } else {
240 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
241 PathBuf::from(home).join(".reticulum")
242 }
243}
244
245pub fn load_or_create_identity(identities_dir: &Path) -> io::Result<Identity> {
247 let id_path = identities_dir.join("identity");
248 if id_path.exists() {
249 load_identity(&id_path)
250 } else {
251 let identity = Identity::new(&mut OsRng);
252 save_identity(&identity, &id_path)?;
253 Ok(identity)
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 use std::sync::atomic::{AtomicU64, Ordering};
262 static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
263
264 fn temp_dir() -> PathBuf {
265 let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
266 let dir = std::env::temp_dir().join(format!("rns-test-{}-{}", std::process::id(), id));
267 let _ = fs::remove_dir_all(&dir);
268 fs::create_dir_all(&dir).unwrap();
269 dir
270 }
271
272 #[test]
273 fn save_load_identity_roundtrip() {
274 let dir = temp_dir();
275 let path = dir.join("test_identity");
276
277 let identity = Identity::new(&mut OsRng);
278 let original_hash = *identity.hash();
279
280 save_identity(&identity, &path).unwrap();
281 let loaded = load_identity(&path).unwrap();
282
283 assert_eq!(*loaded.hash(), original_hash);
284
285 let _ = fs::remove_dir_all(&dir);
286 }
287
288 #[test]
289 fn identity_file_format() {
290 let dir = temp_dir();
291 let path = dir.join("test_identity_fmt");
292
293 let identity = Identity::new(&mut OsRng);
294 save_identity(&identity, &path).unwrap();
295
296 let data = fs::read(&path).unwrap();
297 assert_eq!(data.len(), 64, "Identity file must be exactly 64 bytes");
298
299 let private_key = identity.get_private_key();
302 let private_key = private_key.unwrap();
303 assert_eq!(&data[..], &private_key[..]);
304
305 let _ = fs::remove_dir_all(&dir);
306 }
307
308 #[test]
309 fn save_load_known_destinations_empty() {
310 let dir = temp_dir();
311 let path = dir.join("known_destinations");
312
313 let empty: HashMap<[u8; 16], KnownDestination> = HashMap::new();
314 save_known_destinations(&empty, &path).unwrap();
315
316 let loaded = load_known_destinations(&path).unwrap();
317 assert!(loaded.is_empty());
318
319 let _ = fs::remove_dir_all(&dir);
320 }
321
322 #[test]
323 fn save_load_known_destinations_roundtrip() {
324 let dir = temp_dir();
325 let path = dir.join("known_destinations");
326
327 let mut dests = HashMap::new();
328 dests.insert(
329 [0x01u8; 16],
330 KnownDestination {
331 identity_hash: [0x11u8; 16],
332 public_key: [0xABu8; 64],
333 app_data: Some(vec![0x01, 0x02, 0x03]),
334 hops: 2,
335 received_at: 1700000000.0,
336 receiving_interface: 7,
337 was_used: true,
338 last_used_at: Some(1700000010.0),
339 retained: true,
340 },
341 );
342 dests.insert(
343 [0x02u8; 16],
344 KnownDestination {
345 identity_hash: [0x22u8; 16],
346 public_key: [0xCDu8; 64],
347 app_data: None,
348 hops: 1,
349 received_at: 1700000001.0,
350 receiving_interface: 0,
351 was_used: false,
352 last_used_at: None,
353 retained: false,
354 },
355 );
356
357 save_known_destinations(&dests, &path).unwrap();
358 let loaded = load_known_destinations(&path).unwrap();
359
360 assert_eq!(loaded.len(), 2);
361
362 let d1 = &loaded[&[0x01u8; 16]];
363 assert_eq!(d1.identity_hash, [0x11u8; 16]);
364 assert_eq!(d1.public_key, [0xABu8; 64]);
365 assert_eq!(d1.app_data, Some(vec![0x01, 0x02, 0x03]));
366 assert_eq!(d1.hops, 2);
367 assert_eq!(d1.received_at as u64, 1700000000);
368 assert_eq!(d1.receiving_interface, 7);
369 assert!(d1.was_used);
370 assert_eq!(d1.last_used_at, Some(1700000010.0));
371 assert!(d1.retained);
372
373 let d2 = &loaded[&[0x02u8; 16]];
374 assert_eq!(d2.app_data, None);
375 assert!(!d2.was_used);
376 assert_eq!(d2.last_used_at, None);
377 assert!(!d2.retained);
378
379 let _ = fs::remove_dir_all(&dir);
380 }
381
382 #[test]
383 fn ensure_dirs_creates() {
384 let dir = temp_dir().join("new_config");
385 let _ = fs::remove_dir_all(&dir);
386
387 let paths = ensure_storage_dirs(&dir).unwrap();
388
389 assert!(paths.storage.exists());
390 assert!(paths.cache.exists());
391 assert!(paths.identities.exists());
392 assert!(paths.discovered_interfaces.exists());
393
394 let _ = fs::remove_dir_all(&dir);
395 }
396
397 #[test]
398 fn ensure_dirs_existing() {
399 let dir = temp_dir().join("existing_config");
400 fs::create_dir_all(dir.join("storage")).unwrap();
401 fs::create_dir_all(dir.join("cache")).unwrap();
402
403 let paths = ensure_storage_dirs(&dir).unwrap();
404 assert!(paths.storage.exists());
405 assert!(paths.identities.exists());
406
407 let _ = fs::remove_dir_all(&dir);
408 }
409
410 #[test]
411 fn load_or_create_identity_new() {
412 let dir = temp_dir().join("load_or_create");
413 fs::create_dir_all(&dir).unwrap();
414
415 let identity = load_or_create_identity(&dir).unwrap();
416 let id_path = dir.join("identity");
417 assert!(id_path.exists());
418
419 let loaded = load_or_create_identity(&dir).unwrap();
421 assert_eq!(*identity.hash(), *loaded.hash());
422
423 let _ = fs::remove_dir_all(&dir);
424 }
425}