1#![allow(dead_code)]
2use std::collections::HashMap;
9use std::fmt;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum FingerprintAlgorithm {
14 Crc32,
16 Adler32,
18 XorHash,
20 BlockHash,
22}
23
24impl FingerprintAlgorithm {
25 pub fn name(&self) -> &'static str {
27 match self {
28 Self::Crc32 => "CRC-32",
29 Self::Adler32 => "Adler-32",
30 Self::XorHash => "XOR Hash",
31 Self::BlockHash => "Block Hash",
32 }
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct Fingerprint {
39 pub algorithm: FingerprintAlgorithm,
41 pub hash: String,
43 pub file_size: u64,
45 pub blocks_processed: u64,
47}
48
49impl Fingerprint {
50 pub fn new(algorithm: FingerprintAlgorithm, hash: &str, file_size: u64) -> Self {
52 Self {
53 algorithm,
54 hash: hash.to_string(),
55 file_size,
56 blocks_processed: 0,
57 }
58 }
59
60 pub fn with_blocks(mut self, blocks: u64) -> Self {
62 self.blocks_processed = blocks;
63 self
64 }
65}
66
67impl fmt::Display for Fingerprint {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 write!(f, "{}:{}", self.algorithm.name(), self.hash)
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum VerifyResult {
76 Match,
78 Mismatch {
80 expected: String,
82 actual: String,
84 },
85 SizeChanged {
87 expected: u64,
89 actual: u64,
91 },
92}
93
94impl VerifyResult {
95 pub fn is_ok(&self) -> bool {
97 matches!(self, Self::Match)
98 }
99}
100
101fn compute_crc32(data: &[u8]) -> u32 {
103 let mut crc: u32 = 0xFFFF_FFFF;
104 for &byte in data {
105 crc ^= u32::from(byte);
106 for _ in 0..8 {
107 if crc & 1 != 0 {
108 crc = (crc >> 1) ^ 0xEDB8_8320;
109 } else {
110 crc >>= 1;
111 }
112 }
113 }
114 !crc
115}
116
117fn compute_adler32(data: &[u8]) -> u32 {
119 let mut a: u32 = 1;
120 let mut b: u32 = 0;
121 for &byte in data {
122 a = (a + u32::from(byte)) % 65521;
123 b = (b + a) % 65521;
124 }
125 (b << 16) | a
126}
127
128fn compute_xor_hash(data: &[u8]) -> u32 {
130 let mut hash: u32 = 0;
131 for chunk in data.chunks(4) {
132 let mut val: u32 = 0;
133 for (i, &byte) in chunk.iter().enumerate() {
134 val |= u32::from(byte) << (i * 8);
135 }
136 hash ^= val;
137 }
138 hash
139}
140
141#[allow(clippy::cast_precision_loss)]
143fn compute_block_hash(data: &[u8], block_size: usize) -> (u32, u64) {
144 let mut combined: u32 = 0;
145 let mut blocks: u64 = 0;
146 for chunk in data.chunks(block_size.max(1)) {
147 let block_crc = compute_crc32(chunk);
148 combined = combined.wrapping_add(block_crc);
149 blocks += 1;
150 }
151 (combined, blocks)
152}
153
154pub struct FingerprintEngine {
156 algorithm: FingerprintAlgorithm,
158 block_size: usize,
160 cache: HashMap<String, Fingerprint>,
162}
163
164impl FingerprintEngine {
165 pub fn new(algorithm: FingerprintAlgorithm) -> Self {
167 Self {
168 algorithm,
169 block_size: 4096,
170 cache: HashMap::new(),
171 }
172 }
173
174 pub fn with_block_size(mut self, size: usize) -> Self {
176 self.block_size = size;
177 self
178 }
179
180 #[allow(clippy::cast_precision_loss)]
182 pub fn compute(&self, data: &[u8]) -> Fingerprint {
183 let file_size = data.len() as u64;
184 match self.algorithm {
185 FingerprintAlgorithm::Crc32 => {
186 let crc = compute_crc32(data);
187 Fingerprint::new(self.algorithm, &format!("{crc:08x}"), file_size)
188 }
189 FingerprintAlgorithm::Adler32 => {
190 let adler = compute_adler32(data);
191 Fingerprint::new(self.algorithm, &format!("{adler:08x}"), file_size)
192 }
193 FingerprintAlgorithm::XorHash => {
194 let xor = compute_xor_hash(data);
195 Fingerprint::new(self.algorithm, &format!("{xor:08x}"), file_size)
196 }
197 FingerprintAlgorithm::BlockHash => {
198 let (hash, blocks) = compute_block_hash(data, self.block_size);
199 Fingerprint::new(self.algorithm, &format!("{hash:08x}"), file_size)
200 .with_blocks(blocks)
201 }
202 }
203 }
204
205 pub fn compute_and_cache(&mut self, name: &str, data: &[u8]) -> Fingerprint {
207 let fp = self.compute(data);
208 self.cache.insert(name.to_string(), fp.clone());
209 fp
210 }
211
212 pub fn verify(&self, data: &[u8], expected: &Fingerprint) -> VerifyResult {
214 #[allow(clippy::cast_precision_loss)]
215 let actual_size = data.len() as u64;
216 if actual_size != expected.file_size {
217 return VerifyResult::SizeChanged {
218 expected: expected.file_size,
219 actual: actual_size,
220 };
221 }
222 let actual_fp = self.compute(data);
223 if actual_fp.hash == expected.hash {
224 VerifyResult::Match
225 } else {
226 VerifyResult::Mismatch {
227 expected: expected.hash.clone(),
228 actual: actual_fp.hash,
229 }
230 }
231 }
232
233 pub fn get_cached(&self, name: &str) -> Option<&Fingerprint> {
235 self.cache.get(name)
236 }
237
238 pub fn cache_size(&self) -> usize {
240 self.cache.len()
241 }
242
243 pub fn clear_cache(&mut self) {
245 self.cache.clear();
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 const TEST_DATA: &[u8] = b"Hello, proxy fingerprint test data for OxiMedia framework!";
254
255 #[test]
256 fn test_algorithm_name() {
257 assert_eq!(FingerprintAlgorithm::Crc32.name(), "CRC-32");
258 assert_eq!(FingerprintAlgorithm::Adler32.name(), "Adler-32");
259 assert_eq!(FingerprintAlgorithm::XorHash.name(), "XOR Hash");
260 assert_eq!(FingerprintAlgorithm::BlockHash.name(), "Block Hash");
261 }
262
263 #[test]
264 fn test_crc32_deterministic() {
265 let a = compute_crc32(TEST_DATA);
266 let b = compute_crc32(TEST_DATA);
267 assert_eq!(a, b);
268 }
269
270 #[test]
271 fn test_adler32_deterministic() {
272 let a = compute_adler32(TEST_DATA);
273 let b = compute_adler32(TEST_DATA);
274 assert_eq!(a, b);
275 }
276
277 #[test]
278 fn test_xor_hash_deterministic() {
279 let a = compute_xor_hash(TEST_DATA);
280 let b = compute_xor_hash(TEST_DATA);
281 assert_eq!(a, b);
282 }
283
284 #[test]
285 fn test_crc32_different_data() {
286 let a = compute_crc32(b"hello");
287 let b = compute_crc32(b"world");
288 assert_ne!(a, b);
289 }
290
291 #[test]
292 fn test_compute_crc32_fingerprint() {
293 let engine = FingerprintEngine::new(FingerprintAlgorithm::Crc32);
294 let fp = engine.compute(TEST_DATA);
295 assert_eq!(fp.algorithm, FingerprintAlgorithm::Crc32);
296 assert_eq!(fp.file_size, TEST_DATA.len() as u64);
297 assert!(!fp.hash.is_empty());
298 }
299
300 #[test]
301 fn test_compute_adler32_fingerprint() {
302 let engine = FingerprintEngine::new(FingerprintAlgorithm::Adler32);
303 let fp = engine.compute(TEST_DATA);
304 assert_eq!(fp.algorithm, FingerprintAlgorithm::Adler32);
305 }
306
307 #[test]
308 fn test_compute_block_hash_fingerprint() {
309 let engine = FingerprintEngine::new(FingerprintAlgorithm::BlockHash).with_block_size(16);
310 let fp = engine.compute(TEST_DATA);
311 assert_eq!(fp.algorithm, FingerprintAlgorithm::BlockHash);
312 assert!(fp.blocks_processed > 0);
313 }
314
315 #[test]
316 fn test_verify_match() {
317 let engine = FingerprintEngine::new(FingerprintAlgorithm::Crc32);
318 let fp = engine.compute(TEST_DATA);
319 let result = engine.verify(TEST_DATA, &fp);
320 assert!(result.is_ok());
321 assert_eq!(result, VerifyResult::Match);
322 }
323
324 #[test]
325 fn test_verify_mismatch() {
326 let engine = FingerprintEngine::new(FingerprintAlgorithm::Crc32);
327 let fp = engine.compute(TEST_DATA);
328 let _tampered = b"Tampered data that is different from the original proxy data!";
329 let mut tampered_same_size = TEST_DATA.to_vec();
331 tampered_same_size[0] = b'X';
332 let result = engine.verify(&tampered_same_size, &fp);
333 assert!(!result.is_ok());
334 assert!(matches!(result, VerifyResult::Mismatch { .. }));
335 }
336
337 #[test]
338 fn test_verify_size_changed() {
339 let engine = FingerprintEngine::new(FingerprintAlgorithm::Crc32);
340 let fp = engine.compute(TEST_DATA);
341 let shorter = &TEST_DATA[..10];
342 let result = engine.verify(shorter, &fp);
343 assert!(matches!(result, VerifyResult::SizeChanged { .. }));
344 }
345
346 #[test]
347 fn test_cache_operations() {
348 let mut engine = FingerprintEngine::new(FingerprintAlgorithm::Crc32);
349 assert_eq!(engine.cache_size(), 0);
350 engine.compute_and_cache("proxy_a.mp4", TEST_DATA);
351 assert_eq!(engine.cache_size(), 1);
352 assert!(engine.get_cached("proxy_a.mp4").is_some());
353 assert!(engine.get_cached("nonexistent").is_none());
354 engine.clear_cache();
355 assert_eq!(engine.cache_size(), 0);
356 }
357
358 #[test]
359 fn test_fingerprint_display() {
360 let fp = Fingerprint::new(FingerprintAlgorithm::Crc32, "abcd1234", 100);
361 let display = format!("{fp}");
362 assert_eq!(display, "CRC-32:abcd1234");
363 }
364
365 #[test]
366 fn test_empty_data() {
367 let engine = FingerprintEngine::new(FingerprintAlgorithm::Crc32);
368 let fp = engine.compute(b"");
369 assert_eq!(fp.file_size, 0);
370 let fp2 = engine.compute(b"");
372 assert_eq!(fp.hash, fp2.hash);
373 }
374}