rns_net/
announce_cache.rs1use std::fs;
10use std::io;
11use std::path::PathBuf;
12
13use rns_core::msgpack::{self, Value};
14
15pub struct AnnounceCache {
17 base_path: PathBuf,
18}
19
20impl AnnounceCache {
21 pub fn new(base_path: PathBuf) -> Self {
24 AnnounceCache { base_path }
25 }
26
27 pub fn store(
33 &self,
34 packet_hash: &[u8; 32],
35 raw: &[u8],
36 interface_name: Option<&str>,
37 ) -> io::Result<()> {
38 let filename = hex_encode(packet_hash);
39 let path = self.base_path.join(&filename);
40
41 let iface_val = match interface_name {
42 Some(name) => Value::Str(name.into()),
43 None => Value::Nil,
44 };
45 let data = msgpack::pack(&Value::Array(vec![Value::Bin(raw.to_vec()), iface_val]));
46
47 fs::write(path, data)
48 }
49
50 pub fn get(&self, packet_hash: &[u8; 32]) -> io::Result<Option<(Vec<u8>, Option<String>)>> {
54 let filename = hex_encode(packet_hash);
55 let path = self.base_path.join(&filename);
56
57 if !path.is_file() {
58 return Ok(None);
59 }
60
61 let data = fs::read(&path)?;
62 let (value, _) = msgpack::unpack(&data).map_err(|e| {
63 io::Error::new(io::ErrorKind::InvalidData, format!("msgpack error: {}", e))
64 })?;
65
66 let arr = value
67 .as_array()
68 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Expected msgpack array"))?;
69
70 if arr.is_empty() {
71 return Ok(None);
72 }
73
74 let raw = arr[0]
75 .as_bin()
76 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Expected bin raw bytes"))?;
77
78 let iface_name = if arr.len() > 1 {
79 arr[1].as_str().map(|s| s.to_string())
80 } else {
81 None
82 };
83
84 Ok(Some((raw.to_vec(), iface_name)))
85 }
86
87 pub fn clean(&self, active_hashes: &[[u8; 32]]) -> io::Result<usize> {
92 let entries = match fs::read_dir(&self.base_path) {
93 Ok(e) => e,
94 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(0),
95 Err(e) => return Err(e),
96 };
97
98 let mut removed = 0;
99 for entry in entries {
100 let entry = entry?;
101 let path = entry.path();
102 if !path.is_file() {
103 continue;
104 }
105
106 let filename = match path.file_name().and_then(|n| n.to_str()) {
107 Some(n) => n,
108 None => continue,
109 };
110
111 match hex_decode(filename) {
113 Some(hash) => {
114 if !active_hashes.contains(&hash) {
115 let _ = fs::remove_file(&path);
116 removed += 1;
117 }
118 }
119 None => {
120 let _ = fs::remove_file(&path);
122 removed += 1;
123 }
124 }
125 }
126
127 Ok(removed)
128 }
129
130 #[cfg(test)]
132 pub fn base_path(&self) -> &std::path::Path {
133 &self.base_path
134 }
135}
136
137fn hex_encode(bytes: &[u8; 32]) -> String {
139 let mut s = String::with_capacity(64);
140 for b in bytes {
141 s.push(HEX_CHARS[(b >> 4) as usize]);
142 s.push(HEX_CHARS[(b & 0x0f) as usize]);
143 }
144 s
145}
146
147const HEX_CHARS: [char; 16] = [
148 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
149];
150
151fn hex_decode(s: &str) -> Option<[u8; 32]> {
153 if s.len() != 64 {
154 return None;
155 }
156 let mut result = [0u8; 32];
157 for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
158 let high = hex_nibble(chunk[0])?;
159 let low = hex_nibble(chunk[1])?;
160 result[i] = (high << 4) | low;
161 }
162 Some(result)
163}
164
165fn hex_nibble(c: u8) -> Option<u8> {
166 match c {
167 b'0'..=b'9' => Some(c - b'0'),
168 b'a'..=b'f' => Some(c - b'a' + 10),
169 b'A'..=b'F' => Some(c - b'A' + 10),
170 _ => None,
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use std::sync::atomic::{AtomicU64, Ordering};
178
179 static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
180
181 fn temp_dir() -> PathBuf {
182 let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
183 let dir =
184 std::env::temp_dir().join(format!("rns-announce-cache-{}-{}", std::process::id(), id,));
185 let _ = fs::remove_dir_all(&dir);
186 fs::create_dir_all(&dir).unwrap();
187 dir
188 }
189
190 #[test]
191 fn test_hex_encode_decode_roundtrip() {
192 let hash = [0xAB; 32];
193 let encoded = hex_encode(&hash);
194 assert_eq!(encoded.len(), 64);
195 assert_eq!(encoded.len(), 64);
196 assert!(encoded.chars().all(|c| c == 'a' || c == 'b'));
198 let decoded = hex_decode(&encoded).unwrap();
199 assert_eq!(decoded, hash);
200 }
201
202 #[test]
203 fn test_hex_decode_invalid() {
204 assert!(hex_decode("too_short").is_none());
205 assert!(hex_decode(&"zz".repeat(32)).is_none());
206 }
207
208 #[test]
209 fn test_store_and_get_roundtrip() {
210 let dir = temp_dir();
211 let cache = AnnounceCache::new(dir.clone());
212
213 let hash = [0x42; 32];
214 let raw = vec![0x01, 0x02, 0x03, 0x04, 0x05];
215 cache.store(&hash, &raw, Some("TestInterface")).unwrap();
216
217 let result = cache.get(&hash).unwrap();
218 assert!(result.is_some());
219 let (got_raw, got_name) = result.unwrap();
220 assert_eq!(got_raw, raw);
221 assert_eq!(got_name, Some("TestInterface".to_string()));
222
223 let _ = fs::remove_dir_all(&dir);
224 }
225
226 #[test]
227 fn test_store_with_nil_interface() {
228 let dir = temp_dir();
229 let cache = AnnounceCache::new(dir.clone());
230
231 let hash = [0x55; 32];
232 let raw = vec![0xAA, 0xBB];
233 cache.store(&hash, &raw, None).unwrap();
234
235 let result = cache.get(&hash).unwrap();
236 assert!(result.is_some());
237 let (got_raw, got_name) = result.unwrap();
238 assert_eq!(got_raw, raw);
239 assert_eq!(got_name, None);
240
241 let _ = fs::remove_dir_all(&dir);
242 }
243
244 #[test]
245 fn test_get_nonexistent() {
246 let dir = temp_dir();
247 let cache = AnnounceCache::new(dir.clone());
248
249 let hash = [0xFF; 32];
250 let result = cache.get(&hash).unwrap();
251 assert!(result.is_none());
252
253 let _ = fs::remove_dir_all(&dir);
254 }
255
256 #[test]
257 fn test_clean_removes_stale() {
258 let dir = temp_dir();
259 let cache = AnnounceCache::new(dir.clone());
260
261 let hash1 = [0x11; 32];
262 let hash2 = [0x22; 32];
263 let hash3 = [0x33; 32];
264
265 cache.store(&hash1, &[0x01], None).unwrap();
266 cache.store(&hash2, &[0x02], None).unwrap();
267 cache.store(&hash3, &[0x03], None).unwrap();
268
269 let removed = cache.clean(&[hash2]).unwrap();
271 assert_eq!(removed, 2);
272
273 assert!(cache.get(&hash2).unwrap().is_some());
275 assert!(cache.get(&hash1).unwrap().is_none());
277 assert!(cache.get(&hash3).unwrap().is_none());
278
279 let _ = fs::remove_dir_all(&dir);
280 }
281
282 #[test]
283 fn test_clean_empty_dir() {
284 let dir = temp_dir();
285 let cache = AnnounceCache::new(dir.clone());
286
287 let removed = cache.clean(&[]).unwrap();
288 assert_eq!(removed, 0);
289
290 let _ = fs::remove_dir_all(&dir);
291 }
292
293 #[test]
294 fn test_store_overwrite() {
295 let dir = temp_dir();
296 let cache = AnnounceCache::new(dir.clone());
297
298 let hash = [0x77; 32];
299 cache.store(&hash, &[0x01], Some("iface1")).unwrap();
300 cache.store(&hash, &[0x02, 0x03], Some("iface2")).unwrap();
301
302 let result = cache.get(&hash).unwrap().unwrap();
303 assert_eq!(result.0, vec![0x02, 0x03]);
304 assert_eq!(result.1, Some("iface2".to_string()));
305
306 let _ = fs::remove_dir_all(&dir);
307 }
308}