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