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