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.get_private_key().ok_or_else(|| {
62 io::Error::new(io::ErrorKind::InvalidData, "Identity has no private key")
63 })?;
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(
118 path: &Path,
119) -> io::Result<HashMap<[u8; 16], KnownDestination>> {
120 use rns_core::msgpack;
121
122 let data = fs::read(path)?;
123 if data.is_empty() {
124 return Ok(HashMap::new());
125 }
126
127 let (value, _) = msgpack::unpack(&data).map_err(|e| {
128 io::Error::new(io::ErrorKind::InvalidData, format!("msgpack error: {}", e))
129 })?;
130
131 let map = value.as_map().ok_or_else(|| {
132 io::Error::new(io::ErrorKind::InvalidData, "Expected msgpack map")
133 })?;
134
135 let mut result = HashMap::new();
136
137 for (k, v) in map {
138 let hash_bytes = k.as_bin().ok_or_else(|| {
139 io::Error::new(io::ErrorKind::InvalidData, "Expected bin key")
140 })?;
141
142 if hash_bytes.len() != 16 {
143 continue; }
145
146 let mut dest_hash = [0u8; 16];
147 dest_hash.copy_from_slice(hash_bytes);
148
149 let arr = v.as_array().ok_or_else(|| {
150 io::Error::new(io::ErrorKind::InvalidData, "Expected array value")
151 })?;
152
153 if arr.len() < 3 {
154 continue;
155 }
156
157 let timestamp = arr[0].as_uint().unwrap_or(0) as f64;
158
159 let pkt_hash_bytes = arr[1].as_bin().ok_or_else(|| {
160 io::Error::new(io::ErrorKind::InvalidData, "Expected bin packet_hash")
161 })?;
162 if pkt_hash_bytes.len() != 32 {
163 continue;
164 }
165 let mut packet_hash = [0u8; 32];
166 packet_hash.copy_from_slice(pkt_hash_bytes);
167
168 let pub_key_bytes = arr[2].as_bin().ok_or_else(|| {
169 io::Error::new(io::ErrorKind::InvalidData, "Expected bin public_key")
170 })?;
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() > 3 {
178 arr[3].as_bin().map(|b| b.to_vec())
179 } else {
180 None
181 };
182
183 result.insert(
184 dest_hash,
185 KnownDestination {
186 timestamp,
187 packet_hash,
188 public_key,
189 app_data,
190 },
191 );
192 }
193
194 Ok(result)
195}
196
197pub fn resolve_config_dir(explicit: Option<&Path>) -> PathBuf {
200 if let Some(p) = explicit {
201 p.to_path_buf()
202 } else {
203 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
204 PathBuf::from(home).join(".reticulum")
205 }
206}
207
208pub fn load_or_create_identity(identities_dir: &Path) -> io::Result<Identity> {
210 let id_path = identities_dir.join("identity");
211 if id_path.exists() {
212 load_identity(&id_path)
213 } else {
214 let identity = Identity::new(&mut OsRng);
215 save_identity(&identity, &id_path)?;
216 Ok(identity)
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 use std::sync::atomic::{AtomicU64, Ordering};
225 static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
226
227 fn temp_dir() -> PathBuf {
228 let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
229 let dir = std::env::temp_dir().join(format!(
230 "rns-test-{}-{}",
231 std::process::id(),
232 id
233 ));
234 let _ = fs::remove_dir_all(&dir);
235 fs::create_dir_all(&dir).unwrap();
236 dir
237 }
238
239 #[test]
240 fn save_load_identity_roundtrip() {
241 let dir = temp_dir();
242 let path = dir.join("test_identity");
243
244 let identity = Identity::new(&mut OsRng);
245 let original_hash = *identity.hash();
246
247 save_identity(&identity, &path).unwrap();
248 let loaded = load_identity(&path).unwrap();
249
250 assert_eq!(*loaded.hash(), original_hash);
251
252 let _ = fs::remove_dir_all(&dir);
253 }
254
255 #[test]
256 fn identity_file_format() {
257 let dir = temp_dir();
258 let path = dir.join("test_identity_fmt");
259
260 let identity = Identity::new(&mut OsRng);
261 save_identity(&identity, &path).unwrap();
262
263 let data = fs::read(&path).unwrap();
264 assert_eq!(data.len(), 64, "Identity file must be exactly 64 bytes");
265
266 let private_key = identity.get_private_key();
269 let private_key = private_key.unwrap();
270 assert_eq!(&data[..], &private_key[..]);
271
272 let _ = fs::remove_dir_all(&dir);
273 }
274
275 #[test]
276 fn save_load_known_destinations_empty() {
277 let dir = temp_dir();
278 let path = dir.join("known_destinations");
279
280 let empty: HashMap<[u8; 16], KnownDestination> = HashMap::new();
281 save_known_destinations(&empty, &path).unwrap();
282
283 let loaded = load_known_destinations(&path).unwrap();
284 assert!(loaded.is_empty());
285
286 let _ = fs::remove_dir_all(&dir);
287 }
288
289 #[test]
290 fn save_load_known_destinations_roundtrip() {
291 let dir = temp_dir();
292 let path = dir.join("known_destinations");
293
294 let mut dests = HashMap::new();
295 dests.insert(
296 [0x01u8; 16],
297 KnownDestination {
298 timestamp: 1700000000.0,
299 packet_hash: [0x42u8; 32],
300 public_key: [0xABu8; 64],
301 app_data: Some(vec![0x01, 0x02, 0x03]),
302 },
303 );
304 dests.insert(
305 [0x02u8; 16],
306 KnownDestination {
307 timestamp: 1700000001.0,
308 packet_hash: [0x43u8; 32],
309 public_key: [0xCDu8; 64],
310 app_data: None,
311 },
312 );
313
314 save_known_destinations(&dests, &path).unwrap();
315 let loaded = load_known_destinations(&path).unwrap();
316
317 assert_eq!(loaded.len(), 2);
318
319 let d1 = &loaded[&[0x01u8; 16]];
320 assert_eq!(d1.timestamp as u64, 1700000000);
321 assert_eq!(d1.packet_hash, [0x42u8; 32]);
322 assert_eq!(d1.public_key, [0xABu8; 64]);
323 assert_eq!(d1.app_data, Some(vec![0x01, 0x02, 0x03]));
324
325 let d2 = &loaded[&[0x02u8; 16]];
326 assert_eq!(d2.app_data, None);
327
328 let _ = fs::remove_dir_all(&dir);
329 }
330
331 #[test]
332 fn ensure_dirs_creates() {
333 let dir = temp_dir().join("new_config");
334 let _ = fs::remove_dir_all(&dir);
335
336 let paths = ensure_storage_dirs(&dir).unwrap();
337
338 assert!(paths.storage.exists());
339 assert!(paths.cache.exists());
340 assert!(paths.identities.exists());
341 assert!(paths.discovered_interfaces.exists());
342
343 let _ = fs::remove_dir_all(&dir);
344 }
345
346 #[test]
347 fn ensure_dirs_existing() {
348 let dir = temp_dir().join("existing_config");
349 fs::create_dir_all(dir.join("storage")).unwrap();
350 fs::create_dir_all(dir.join("cache")).unwrap();
351
352 let paths = ensure_storage_dirs(&dir).unwrap();
353 assert!(paths.storage.exists());
354 assert!(paths.identities.exists());
355
356 let _ = fs::remove_dir_all(&dir);
357 }
358
359 #[test]
360 fn load_or_create_identity_new() {
361 let dir = temp_dir().join("load_or_create");
362 fs::create_dir_all(&dir).unwrap();
363
364 let identity = load_or_create_identity(&dir).unwrap();
365 let id_path = dir.join("identity");
366 assert!(id_path.exists());
367
368 let loaded = load_or_create_identity(&dir).unwrap();
370 assert_eq!(*identity.hash(), *loaded.hash());
371
372 let _ = fs::remove_dir_all(&dir);
373 }
374}