oximedia_proxy/
linking.rs1#![allow(dead_code)]
8#![allow(missing_docs)]
9
10#[must_use]
16fn fnv1a_64(data: &[u8]) -> u64 {
17 const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
18 const PRIME: u64 = 0x0000_0100_0000_01b3;
19 let mut hash = OFFSET_BASIS;
20 for &byte in data {
21 hash ^= u64::from(byte);
22 hash = hash.wrapping_mul(PRIME);
23 }
24 hash
25}
26
27#[derive(Debug, Clone, PartialEq)]
33pub struct ProxyLink {
34 pub proxy_path: String,
36 pub original_path: String,
38 pub checksum: u64,
40 pub created_ms: u64,
42}
43
44impl ProxyLink {
45 #[must_use]
47 pub fn new(
48 proxy_path: impl Into<String>,
49 original_path: impl Into<String>,
50 checksum: u64,
51 created_ms: u64,
52 ) -> Self {
53 Self {
54 proxy_path: proxy_path.into(),
55 original_path: original_path.into(),
56 checksum,
57 created_ms,
58 }
59 }
60
61 #[must_use]
65 pub fn is_valid_checksum(&self, data: &[u8]) -> bool {
66 fnv1a_64(data) == self.checksum
67 }
68}
69
70#[derive(Debug, Default)]
76pub struct ProxyLinkRegistry {
77 pub links: Vec<ProxyLink>,
79}
80
81impl ProxyLinkRegistry {
82 #[must_use]
84 pub fn new() -> Self {
85 Self::default()
86 }
87
88 pub fn register(&mut self, proxy: &str, original: &str, checksum: u64) {
92 self.links.retain(|l| l.proxy_path != proxy);
94 self.links
95 .push(ProxyLink::new(proxy, original, checksum, 0));
96 }
97
98 #[must_use]
102 pub fn find_original(&self, proxy: &str) -> Option<&str> {
103 self.links
104 .iter()
105 .find(|l| l.proxy_path == proxy)
106 .map(|l| l.original_path.as_str())
107 }
108
109 #[must_use]
113 pub fn find_proxy(&self, original: &str) -> Option<&str> {
114 self.links
115 .iter()
116 .find(|l| l.original_path == original)
117 .map(|l| l.proxy_path.as_str())
118 }
119
120 pub fn unlink(&mut self, proxy: &str) -> bool {
124 let before = self.links.len();
125 self.links.retain(|l| l.proxy_path != proxy);
126 self.links.len() < before
127 }
128
129 #[must_use]
131 pub fn is_linked(&self, proxy: &str) -> bool {
132 self.links.iter().any(|l| l.proxy_path == proxy)
133 }
134}
135
136#[derive(Debug, PartialEq)]
142pub enum ReconnectResult {
143 Found(String),
145 NotFound,
147 Ambiguous(Vec<String>),
149}
150
151pub struct ProxyReconnector;
157
158impl ProxyReconnector {
159 #[must_use]
169 pub fn reconnect(proxy: &str, search_paths: &[String]) -> ReconnectResult {
170 let key = proxy.rsplit('/').next().unwrap_or(proxy);
172
173 let matches: Vec<String> = search_paths
174 .iter()
175 .filter(|p| p.ends_with(key))
176 .cloned()
177 .collect();
178
179 match matches.len() {
180 0 => ReconnectResult::NotFound,
181 1 => ReconnectResult::Found(
182 matches
183 .into_iter()
184 .next()
185 .expect("invariant: matches.len() == 1 guarantees a first element"),
186 ),
187 _ => ReconnectResult::Ambiguous(matches),
188 }
189 }
190}
191
192#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_fnv1a_empty() {
202 let h1 = fnv1a_64(b"");
204 let h2 = fnv1a_64(b"");
205 assert_eq!(h1, h2);
206 }
207
208 #[test]
209 fn test_fnv1a_different_inputs() {
210 assert_ne!(fnv1a_64(b"hello"), fnv1a_64(b"world"));
211 }
212
213 #[test]
214 fn test_proxy_link_is_valid_checksum_pass() {
215 let data = b"test content";
216 let checksum = fnv1a_64(data);
217 let link = ProxyLink::new("proxy.mp4", "original.mov", checksum, 0);
218 assert!(link.is_valid_checksum(data));
219 }
220
221 #[test]
222 fn test_proxy_link_is_valid_checksum_fail() {
223 let link = ProxyLink::new("proxy.mp4", "original.mov", 0xdeadbeef, 0);
224 assert!(!link.is_valid_checksum(b"some data"));
225 }
226
227 #[test]
228 fn test_registry_register_and_find_original() {
229 let mut reg = ProxyLinkRegistry::new();
230 reg.register("p.mp4", "o.mov", 0);
231 assert_eq!(reg.find_original("p.mp4"), Some("o.mov"));
232 }
233
234 #[test]
235 fn test_registry_find_proxy() {
236 let mut reg = ProxyLinkRegistry::new();
237 reg.register("p.mp4", "o.mov", 0);
238 assert_eq!(reg.find_proxy("o.mov"), Some("p.mp4"));
239 }
240
241 #[test]
242 fn test_registry_find_missing_returns_none() {
243 let reg = ProxyLinkRegistry::new();
244 assert!(reg.find_original("does_not_exist.mp4").is_none());
245 }
246
247 #[test]
248 fn test_registry_unlink_existing() {
249 let mut reg = ProxyLinkRegistry::new();
250 reg.register("p.mp4", "o.mov", 0);
251 let removed = reg.unlink("p.mp4");
252 assert!(removed);
253 assert!(!reg.is_linked("p.mp4"));
254 }
255
256 #[test]
257 fn test_registry_unlink_missing_returns_false() {
258 let mut reg = ProxyLinkRegistry::new();
259 assert!(!reg.unlink("no_such.mp4"));
260 }
261
262 #[test]
263 fn test_registry_is_linked() {
264 let mut reg = ProxyLinkRegistry::new();
265 reg.register("p.mp4", "o.mov", 0);
266 assert!(reg.is_linked("p.mp4"));
267 }
268
269 #[test]
270 fn test_registry_register_replaces_existing() {
271 let mut reg = ProxyLinkRegistry::new();
272 reg.register("p.mp4", "original1.mov", 0);
273 reg.register("p.mp4", "original2.mov", 0);
274 assert_eq!(reg.find_original("p.mp4"), Some("original2.mov"));
275 assert_eq!(reg.links.len(), 1);
276 }
277
278 #[test]
279 fn test_reconnector_found() {
280 let paths = vec![
281 "/media/archive/clip001.mov".to_string(),
282 "/media/archive/clip002.mov".to_string(),
283 ];
284 let result = ProxyReconnector::reconnect("proxy/clip001.mov", &paths);
285 assert_eq!(
286 result,
287 ReconnectResult::Found("/media/archive/clip001.mov".to_string())
288 );
289 }
290
291 #[test]
292 fn test_reconnector_not_found() {
293 let paths = vec!["/media/archive/other.mov".to_string()];
294 let result = ProxyReconnector::reconnect("proxy/clip001.mov", &paths);
295 assert_eq!(result, ReconnectResult::NotFound);
296 }
297
298 #[test]
299 fn test_reconnector_ambiguous() {
300 let paths = vec![
301 "/drive1/clip001.mov".to_string(),
302 "/drive2/clip001.mov".to_string(),
303 ];
304 let result = ProxyReconnector::reconnect("proxy/clip001.mov", &paths);
305 assert!(matches!(result, ReconnectResult::Ambiguous(_)));
306 }
307}