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 timestamp: f64,
31 pub packet_hash: [u8; 32],
32 pub public_key: [u8; 64],
33 pub app_data: Option<Vec<u8>>,
34}
35
36pub fn ensure_storage_dirs(config_dir: &Path) -> io::Result<StoragePaths> {
38 let storage = config_dir.join("storage");
39 let cache = config_dir.join("cache");
40 let identities = storage.join("identities");
41 let announces = cache.join("announces");
42 let discovered_interfaces = storage.join("discovery").join("interfaces");
43
44 fs::create_dir_all(&storage)?;
45 fs::create_dir_all(&cache)?;
46 fs::create_dir_all(&identities)?;
47 fs::create_dir_all(&announces)?;
48 fs::create_dir_all(&discovered_interfaces)?;
49
50 Ok(StoragePaths {
51 config_dir: config_dir.to_path_buf(),
52 storage,
53 cache,
54 identities,
55 discovered_interfaces,
56 })
57}
58
59pub fn save_identity(identity: &Identity, path: &Path) -> io::Result<()> {
61 let private_key = identity
62 .get_private_key()
63 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Identity has no private key"))?;
64 fs::write(path, &private_key)
65}
66
67pub fn load_identity(path: &Path) -> io::Result<Identity> {
69 let data = fs::read(path)?;
70 if data.len() != 64 {
71 return Err(io::Error::new(
72 io::ErrorKind::InvalidData,
73 format!("Identity file must be 64 bytes, got {}", data.len()),
74 ));
75 }
76 let mut key = [0u8; 64];
77 key.copy_from_slice(&data);
78 Ok(Identity::from_private_key(&key))
79}
80
81pub fn save_known_destinations(
85 destinations: &HashMap<[u8; 16], KnownDestination>,
86 path: &Path,
87) -> io::Result<()> {
88 use rns_core::msgpack::{self, Value};
89
90 let entries: Vec<(Value, Value)> = destinations
91 .iter()
92 .map(|(hash, dest)| {
93 let key = Value::Bin(hash.to_vec());
94 let app_data = match &dest.app_data {
95 Some(d) => Value::Bin(d.clone()),
96 None => Value::Nil,
97 };
98 let value = Value::Array(vec![
99 Value::UInt(dest.timestamp as u64),
104 Value::Bin(dest.packet_hash.to_vec()),
105 Value::Bin(dest.public_key.to_vec()),
106 app_data,
107 ]);
108 (key, value)
109 })
110 .collect();
111
112 let packed = msgpack::pack(&Value::Map(entries));
113 fs::write(path, packed)
114}
115
116pub fn load_known_destinations(path: &Path) -> io::Result<HashMap<[u8; 16], KnownDestination>> {
118 use rns_core::msgpack;
119
120 let data = fs::read(path)?;
121 if data.is_empty() {
122 return Ok(HashMap::new());
123 }
124
125 let (value, _) = msgpack::unpack(&data)
126 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("msgpack error: {}", e)))?;
127
128 let map = value
129 .as_map()
130 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Expected msgpack map"))?;
131
132 let mut result = HashMap::new();
133
134 for (k, v) in map {
135 let hash_bytes = k
136 .as_bin()
137 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Expected bin key"))?;
138
139 if hash_bytes.len() != 16 {
140 continue; }
142
143 let mut dest_hash = [0u8; 16];
144 dest_hash.copy_from_slice(hash_bytes);
145
146 let arr = v
147 .as_array()
148 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Expected array value"))?;
149
150 if arr.len() < 3 {
151 continue;
152 }
153
154 let timestamp = arr[0].as_uint().unwrap_or(0) as f64;
155
156 let pkt_hash_bytes = arr[1].as_bin().ok_or_else(|| {
157 io::Error::new(io::ErrorKind::InvalidData, "Expected bin packet_hash")
158 })?;
159 if pkt_hash_bytes.len() != 32 {
160 continue;
161 }
162 let mut packet_hash = [0u8; 32];
163 packet_hash.copy_from_slice(pkt_hash_bytes);
164
165 let pub_key_bytes = arr[2]
166 .as_bin()
167 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Expected bin public_key"))?;
168 if pub_key_bytes.len() != 64 {
169 continue;
170 }
171 let mut public_key = [0u8; 64];
172 public_key.copy_from_slice(pub_key_bytes);
173
174 let app_data = if arr.len() > 3 {
175 arr[3].as_bin().map(|b| b.to_vec())
176 } else {
177 None
178 };
179
180 result.insert(
181 dest_hash,
182 KnownDestination {
183 timestamp,
184 packet_hash,
185 public_key,
186 app_data,
187 },
188 );
189 }
190
191 Ok(result)
192}
193
194pub fn resolve_config_dir(explicit: Option<&Path>) -> PathBuf {
197 if let Some(p) = explicit {
198 p.to_path_buf()
199 } else {
200 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
201 PathBuf::from(home).join(".reticulum")
202 }
203}
204
205pub fn load_or_create_identity(identities_dir: &Path) -> io::Result<Identity> {
207 let id_path = identities_dir.join("identity");
208 if id_path.exists() {
209 load_identity(&id_path)
210 } else {
211 let identity = Identity::new(&mut OsRng);
212 save_identity(&identity, &id_path)?;
213 Ok(identity)
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 use std::sync::atomic::{AtomicU64, Ordering};
222 static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
223
224 fn temp_dir() -> PathBuf {
225 let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
226 let dir = std::env::temp_dir().join(format!("rns-test-{}-{}", std::process::id(), id));
227 let _ = fs::remove_dir_all(&dir);
228 fs::create_dir_all(&dir).unwrap();
229 dir
230 }
231
232 #[test]
233 fn save_load_identity_roundtrip() {
234 let dir = temp_dir();
235 let path = dir.join("test_identity");
236
237 let identity = Identity::new(&mut OsRng);
238 let original_hash = *identity.hash();
239
240 save_identity(&identity, &path).unwrap();
241 let loaded = load_identity(&path).unwrap();
242
243 assert_eq!(*loaded.hash(), original_hash);
244
245 let _ = fs::remove_dir_all(&dir);
246 }
247
248 #[test]
249 fn identity_file_format() {
250 let dir = temp_dir();
251 let path = dir.join("test_identity_fmt");
252
253 let identity = Identity::new(&mut OsRng);
254 save_identity(&identity, &path).unwrap();
255
256 let data = fs::read(&path).unwrap();
257 assert_eq!(data.len(), 64, "Identity file must be exactly 64 bytes");
258
259 let private_key = identity.get_private_key();
262 let private_key = private_key.unwrap();
263 assert_eq!(&data[..], &private_key[..]);
264
265 let _ = fs::remove_dir_all(&dir);
266 }
267
268 #[test]
269 fn save_load_known_destinations_empty() {
270 let dir = temp_dir();
271 let path = dir.join("known_destinations");
272
273 let empty: HashMap<[u8; 16], KnownDestination> = HashMap::new();
274 save_known_destinations(&empty, &path).unwrap();
275
276 let loaded = load_known_destinations(&path).unwrap();
277 assert!(loaded.is_empty());
278
279 let _ = fs::remove_dir_all(&dir);
280 }
281
282 #[test]
283 fn save_load_known_destinations_roundtrip() {
284 let dir = temp_dir();
285 let path = dir.join("known_destinations");
286
287 let mut dests = HashMap::new();
288 dests.insert(
289 [0x01u8; 16],
290 KnownDestination {
291 timestamp: 1700000000.0,
292 packet_hash: [0x42u8; 32],
293 public_key: [0xABu8; 64],
294 app_data: Some(vec![0x01, 0x02, 0x03]),
295 },
296 );
297 dests.insert(
298 [0x02u8; 16],
299 KnownDestination {
300 timestamp: 1700000001.0,
301 packet_hash: [0x43u8; 32],
302 public_key: [0xCDu8; 64],
303 app_data: None,
304 },
305 );
306
307 save_known_destinations(&dests, &path).unwrap();
308 let loaded = load_known_destinations(&path).unwrap();
309
310 assert_eq!(loaded.len(), 2);
311
312 let d1 = &loaded[&[0x01u8; 16]];
313 assert_eq!(d1.timestamp as u64, 1700000000);
314 assert_eq!(d1.packet_hash, [0x42u8; 32]);
315 assert_eq!(d1.public_key, [0xABu8; 64]);
316 assert_eq!(d1.app_data, Some(vec![0x01, 0x02, 0x03]));
317
318 let d2 = &loaded[&[0x02u8; 16]];
319 assert_eq!(d2.app_data, None);
320
321 let _ = fs::remove_dir_all(&dir);
322 }
323
324 #[test]
325 fn ensure_dirs_creates() {
326 let dir = temp_dir().join("new_config");
327 let _ = fs::remove_dir_all(&dir);
328
329 let paths = ensure_storage_dirs(&dir).unwrap();
330
331 assert!(paths.storage.exists());
332 assert!(paths.cache.exists());
333 assert!(paths.identities.exists());
334 assert!(paths.discovered_interfaces.exists());
335
336 let _ = fs::remove_dir_all(&dir);
337 }
338
339 #[test]
340 fn ensure_dirs_existing() {
341 let dir = temp_dir().join("existing_config");
342 fs::create_dir_all(dir.join("storage")).unwrap();
343 fs::create_dir_all(dir.join("cache")).unwrap();
344
345 let paths = ensure_storage_dirs(&dir).unwrap();
346 assert!(paths.storage.exists());
347 assert!(paths.identities.exists());
348
349 let _ = fs::remove_dir_all(&dir);
350 }
351
352 #[test]
353 fn load_or_create_identity_new() {
354 let dir = temp_dir().join("load_or_create");
355 fs::create_dir_all(&dir).unwrap();
356
357 let identity = load_or_create_identity(&dir).unwrap();
358 let id_path = dir.join("identity");
359 assert!(id_path.exists());
360
361 let loaded = load_or_create_identity(&dir).unwrap();
363 assert_eq!(*identity.hash(), *loaded.hash());
364
365 let _ = fs::remove_dir_all(&dir);
366 }
367}