Skip to main content

oximedia_proxy/
proxy_fingerprint.rs

1#![allow(dead_code)]
2//! Proxy fingerprinting for integrity verification.
3//!
4//! Generates and verifies content-based fingerprints for proxy files to ensure
5//! they have not been corrupted or tampered with during transfer, storage,
6//! or editing workflows.
7
8use std::collections::HashMap;
9use std::fmt;
10
11/// Hash algorithm used for fingerprinting.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum FingerprintAlgorithm {
14    /// CRC-32 (fast but weak).
15    Crc32,
16    /// Adler-32 (fast checksum).
17    Adler32,
18    /// Simple XOR-based hash (very fast, low quality).
19    XorHash,
20    /// Block-level content hash.
21    BlockHash,
22}
23
24impl FingerprintAlgorithm {
25    /// Human-readable name.
26    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/// A content fingerprint for a proxy file.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct Fingerprint {
39    /// The algorithm used to generate this fingerprint.
40    pub algorithm: FingerprintAlgorithm,
41    /// The fingerprint value as a hex string.
42    pub hash: String,
43    /// File size in bytes at the time of fingerprinting.
44    pub file_size: u64,
45    /// Number of blocks processed.
46    pub blocks_processed: u64,
47}
48
49impl Fingerprint {
50    /// Create a new fingerprint.
51    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    /// Set the blocks processed count.
61    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/// Result of a fingerprint verification.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum VerifyResult {
76    /// Fingerprints match.
77    Match,
78    /// Fingerprints do not match.
79    Mismatch {
80        /// Expected hash.
81        expected: String,
82        /// Actual hash.
83        actual: String,
84    },
85    /// File size changed.
86    SizeChanged {
87        /// Expected size.
88        expected: u64,
89        /// Actual size.
90        actual: u64,
91    },
92}
93
94impl VerifyResult {
95    /// Whether verification passed.
96    pub fn is_ok(&self) -> bool {
97        matches!(self, Self::Match)
98    }
99}
100
101/// Simple CRC-32 computation (non-cryptographic, for proxy integrity only).
102fn 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
117/// Simple Adler-32 computation.
118fn 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
128/// Simple XOR hash.
129fn 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/// Block-level hash: hash each block and combine.
142#[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
154/// Engine for computing and verifying proxy fingerprints.
155pub struct FingerprintEngine {
156    /// Default algorithm.
157    algorithm: FingerprintAlgorithm,
158    /// Block size for block-based hashing.
159    block_size: usize,
160    /// Cache of computed fingerprints.
161    cache: HashMap<String, Fingerprint>,
162}
163
164impl FingerprintEngine {
165    /// Create a new fingerprint engine.
166    pub fn new(algorithm: FingerprintAlgorithm) -> Self {
167        Self {
168            algorithm,
169            block_size: 4096,
170            cache: HashMap::new(),
171        }
172    }
173
174    /// Set the block size for block-based hashing.
175    pub fn with_block_size(mut self, size: usize) -> Self {
176        self.block_size = size;
177        self
178    }
179
180    /// Compute a fingerprint for the given data.
181    #[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    /// Compute and cache a fingerprint for a named proxy.
206    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    /// Verify data against a stored fingerprint.
213    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    /// Look up a cached fingerprint by name.
234    pub fn get_cached(&self, name: &str) -> Option<&Fingerprint> {
235        self.cache.get(name)
236    }
237
238    /// Number of cached fingerprints.
239    pub fn cache_size(&self) -> usize {
240        self.cache.len()
241    }
242
243    /// Clear the fingerprint cache.
244    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        // Make tampered same length as TEST_DATA for size match
330        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        // CRC32 of empty data should be deterministic
371        let fp2 = engine.compute(b"");
372        assert_eq!(fp.hash, fp2.hash);
373    }
374}