Skip to main content

apfsds_protocol/
validation.rs

1//! Frame validation utilities
2
3use crate::frame::{ArchivedProxyFrame, ProxyFrame};
4use thiserror::Error;
5
6/// Validation errors
7#[derive(Error, Debug)]
8pub enum ValidationError {
9    #[error("Duplicate frame UUID detected")]
10    DuplicateUuid,
11
12    #[error("Checksum mismatch: expected {expected}, got {actual}")]
13    ChecksumMismatch { expected: u32, actual: u32 },
14
15    #[error("Timestamp out of range: {0}ms drift")]
16    TimestampOutOfRange(i64),
17
18    #[error("Invalid connection ID")]
19    InvalidConnId,
20
21    #[error("Payload too large: {size} bytes (max: {max})")]
22    PayloadTooLarge { size: usize, max: usize },
23}
24
25/// Maximum allowed payload size (64KB)
26pub const MAX_PAYLOAD_SIZE: usize = 65536;
27
28/// Maximum allowed timestamp drift (30 seconds)
29pub const MAX_TIMESTAMP_DRIFT_MS: i64 = 30_000;
30
31/// Validate a ProxyFrame
32pub fn validate_frame(frame: &ProxyFrame, current_time_ms: u64) -> Result<(), ValidationError> {
33    // Check payload size
34    if frame.payload.len() > MAX_PAYLOAD_SIZE {
35        return Err(ValidationError::PayloadTooLarge {
36            size: frame.payload.len(),
37            max: MAX_PAYLOAD_SIZE,
38        });
39    }
40
41    // Verify checksum
42    let computed = crc32fast::hash(&frame.payload);
43    if computed != frame.checksum {
44        return Err(ValidationError::ChecksumMismatch {
45            expected: frame.checksum,
46            actual: computed,
47        });
48    }
49
50    // Check timestamp
51    let drift = current_time_ms as i64 - frame.timestamp as i64;
52    if drift.abs() > MAX_TIMESTAMP_DRIFT_MS {
53        return Err(ValidationError::TimestampOutOfRange(drift));
54    }
55
56    Ok(())
57}
58
59/// Validate an archived frame (zero-copy)
60pub fn validate_archived_frame(
61    frame: &ArchivedProxyFrame,
62    current_time_ms: u64,
63) -> Result<(), ValidationError> {
64    // Check payload size
65    if frame.payload.len() > MAX_PAYLOAD_SIZE {
66        return Err(ValidationError::PayloadTooLarge {
67            size: frame.payload.len(),
68            max: MAX_PAYLOAD_SIZE,
69        });
70    }
71
72    // Verify checksum - convert from rkyv's archived type
73    let frame_checksum: u32 = frame.checksum.to_native();
74    let computed = crc32fast::hash(&frame.payload);
75    if computed != frame_checksum {
76        return Err(ValidationError::ChecksumMismatch {
77            expected: frame_checksum,
78            actual: computed,
79        });
80    }
81
82    // Check timestamp - convert from rkyv's archived type
83    let frame_timestamp: u64 = frame.timestamp.to_native();
84    let drift = current_time_ms as i64 - frame_timestamp as i64;
85    if drift.abs() > MAX_TIMESTAMP_DRIFT_MS {
86        return Err(ValidationError::TimestampOutOfRange(drift));
87    }
88
89    Ok(())
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_valid_frame() {
98        let frame = ProxyFrame::new_data(1, [0; 16], 443, vec![1, 2, 3]);
99        let result = validate_frame(&frame, frame.timestamp);
100        assert!(result.is_ok());
101    }
102
103    #[test]
104    fn test_checksum_mismatch() {
105        let mut frame = ProxyFrame::new_data(1, [0; 16], 443, vec![1, 2, 3]);
106        frame.checksum = 0xDEADBEEF; // Wrong checksum
107
108        let result = validate_frame(&frame, frame.timestamp);
109        assert!(matches!(
110            result,
111            Err(ValidationError::ChecksumMismatch { .. })
112        ));
113    }
114
115    #[test]
116    fn test_timestamp_drift() {
117        let frame = ProxyFrame::new_data(1, [0; 16], 443, vec![1, 2, 3]);
118        let future_time = frame.timestamp + 60_000; // 60 seconds later
119
120        let result = validate_frame(&frame, future_time);
121        assert!(matches!(
122            result,
123            Err(ValidationError::TimestampOutOfRange(_))
124        ));
125    }
126
127    #[test]
128    fn test_payload_too_large() {
129        let large_payload = vec![0u8; MAX_PAYLOAD_SIZE + 1];
130        let frame = ProxyFrame::new_data(1, [0; 16], 443, large_payload);
131
132        let result = validate_frame(&frame, frame.timestamp);
133        assert!(matches!(
134            result,
135            Err(ValidationError::PayloadTooLarge { .. })
136        ));
137    }
138}