1use chie_shared::{BandwidthProof, CHUNK_SIZE, ChunkRequest, ChunkResponse};
7use std::time::{Duration, SystemTime};
8
9#[derive(Debug, thiserror::Error)]
11pub enum ContentValidationError {
12 #[error("Invalid content size: {0} bytes")]
13 InvalidContentSize(u64),
14
15 #[error("Invalid chunk index: {index} out of {total}")]
16 InvalidChunkIndex { index: u64, total: u64 },
17
18 #[error("Invalid timestamp: {0}")]
19 InvalidTimestamp(String),
20
21 #[error("Invalid signature length: expected {expected}, got {actual}")]
22 InvalidSignatureLength { expected: usize, actual: usize },
23
24 #[error("Invalid public key length: expected {expected}, got {actual}")]
25 InvalidPublicKeyLength { expected: usize, actual: usize },
26
27 #[error("Invalid bandwidth value: {0}")]
28 InvalidBandwidth(String),
29
30 #[error("Content too large: {size} bytes exceeds max {max} bytes")]
31 ContentTooLarge { size: u64, max: u64 },
32
33 #[error("Content too small: {size} bytes is below min {min} bytes")]
34 ContentTooSmall { size: u64, min: u64 },
35}
36
37pub struct ContentLimits {
39 pub min_size: u64,
41 pub max_size: u64,
43 pub max_chunks: u64,
45}
46
47impl Default for ContentLimits {
48 fn default() -> Self {
49 Self {
50 min_size: 1024, max_size: 10 * 1024 * 1024 * 1024, max_chunks: 100_000, }
54 }
55}
56
57#[inline]
59pub fn validate_content_size(
60 size: u64,
61 limits: &ContentLimits,
62) -> Result<(), ContentValidationError> {
63 if size < limits.min_size {
64 return Err(ContentValidationError::ContentTooSmall {
65 size,
66 min: limits.min_size,
67 });
68 }
69
70 if size > limits.max_size {
71 return Err(ContentValidationError::ContentTooLarge {
72 size,
73 max: limits.max_size,
74 });
75 }
76
77 Ok(())
78}
79
80#[inline]
82pub fn validate_chunk_index(
83 chunk_index: u64,
84 total_chunks: u64,
85) -> Result<(), ContentValidationError> {
86 if chunk_index >= total_chunks {
87 return Err(ContentValidationError::InvalidChunkIndex {
88 index: chunk_index,
89 total: total_chunks,
90 });
91 }
92
93 Ok(())
94}
95
96#[inline]
98pub fn validate_request_timestamp(
99 request: &ChunkRequest,
100 max_age: Duration,
101) -> Result<(), ContentValidationError> {
102 let now = SystemTime::now();
103 let request_time = SystemTime::UNIX_EPOCH + Duration::from_millis(request.timestamp_ms as u64);
104
105 let age = now.duration_since(request_time).map_err(|_| {
106 ContentValidationError::InvalidTimestamp("Request timestamp is in the future".to_string())
107 })?;
108
109 if age > max_age {
110 return Err(ContentValidationError::InvalidTimestamp(format!(
111 "Request is too old: {:?} > {:?}",
112 age, max_age
113 )));
114 }
115
116 Ok(())
117}
118
119#[inline]
121pub fn validate_response_signature(response: &ChunkResponse) -> Result<(), ContentValidationError> {
122 const ED25519_SIG_LEN: usize = 64;
124
125 if response.provider_signature.len() != ED25519_SIG_LEN {
126 return Err(ContentValidationError::InvalidSignatureLength {
127 expected: ED25519_SIG_LEN,
128 actual: response.provider_signature.len(),
129 });
130 }
131
132 if response.provider_public_key.len() != 32 {
133 return Err(ContentValidationError::InvalidPublicKeyLength {
134 expected: 32,
135 actual: response.provider_public_key.len(),
136 });
137 }
138
139 Ok(())
140}
141
142#[inline]
144pub fn validate_proof_structure(proof: &BandwidthProof) -> Result<(), ContentValidationError> {
145 const ED25519_SIG_LEN: usize = 64;
147 const ED25519_KEY_LEN: usize = 32;
148
149 if proof.provider_signature.len() != ED25519_SIG_LEN {
150 return Err(ContentValidationError::InvalidSignatureLength {
151 expected: ED25519_SIG_LEN,
152 actual: proof.provider_signature.len(),
153 });
154 }
155
156 if proof.requester_signature.len() != ED25519_SIG_LEN {
157 return Err(ContentValidationError::InvalidSignatureLength {
158 expected: ED25519_SIG_LEN,
159 actual: proof.requester_signature.len(),
160 });
161 }
162
163 if proof.provider_public_key.len() != ED25519_KEY_LEN {
164 return Err(ContentValidationError::InvalidPublicKeyLength {
165 expected: ED25519_KEY_LEN,
166 actual: proof.provider_public_key.len(),
167 });
168 }
169
170 if proof.requester_public_key.len() != ED25519_KEY_LEN {
171 return Err(ContentValidationError::InvalidPublicKeyLength {
172 expected: ED25519_KEY_LEN,
173 actual: proof.requester_public_key.len(),
174 });
175 }
176
177 if proof.start_timestamp_ms >= proof.end_timestamp_ms {
179 return Err(ContentValidationError::InvalidTimestamp(
180 "Start timestamp must be before end timestamp".to_string(),
181 ));
182 }
183
184 if proof.bytes_transferred == 0 {
186 return Err(ContentValidationError::InvalidBandwidth(
187 "Bytes transferred cannot be zero".to_string(),
188 ));
189 }
190
191 Ok(())
192}
193
194#[must_use]
196#[inline]
197pub const fn calculate_expected_chunks(content_size: u64) -> u64 {
198 let chunks = content_size / CHUNK_SIZE as u64;
199 let remainder = content_size % CHUNK_SIZE as u64;
200
201 if remainder > 0 {
202 chunks + 1
203 } else if chunks == 0 {
204 1
205 } else {
206 chunks
207 }
208}
209
210#[inline]
212pub fn validate_bandwidth(
213 bytes: u64,
214 duration_ms: u64,
215 max_bandwidth_mbps: f64,
216) -> Result<(), ContentValidationError> {
217 if duration_ms == 0 {
218 return Err(ContentValidationError::InvalidBandwidth(
219 "Duration cannot be zero".to_string(),
220 ));
221 }
222
223 let bits = bytes as f64 * 8.0;
225 let seconds = duration_ms as f64 / 1000.0;
226 let mbps = bits / seconds / 1_000_000.0;
227
228 let max_mbps = max_bandwidth_mbps.max(10_000.0);
230 if mbps > max_mbps {
231 return Err(ContentValidationError::InvalidBandwidth(format!(
232 "Bandwidth {:.2} Mbps exceeds maximum {:.2} Mbps",
233 mbps, max_mbps
234 )));
235 }
236
237 Ok(())
238}
239
240#[must_use]
242#[inline]
243pub fn sanitize_cid(cid: &str) -> String {
244 cid.chars()
246 .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
247 .collect()
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use chie_crypto::KeyPair;
254 use chie_shared::ChunkRequest;
255
256 #[test]
257 fn test_validate_content_size() {
258 let limits = ContentLimits::default();
259
260 assert!(validate_content_size(1024 * 1024, &limits).is_ok());
262
263 assert!(validate_content_size(512, &limits).is_err());
265
266 assert!(validate_content_size(20 * 1024 * 1024 * 1024, &limits).is_err());
268 }
269
270 #[test]
271 fn test_validate_chunk_index() {
272 assert!(validate_chunk_index(0, 10).is_ok());
273 assert!(validate_chunk_index(9, 10).is_ok());
274 assert!(validate_chunk_index(10, 10).is_err());
275 assert!(validate_chunk_index(100, 10).is_err());
276 }
277
278 #[test]
279 fn test_validate_request_timestamp() {
280 let keypair = KeyPair::generate();
281 let request = ChunkRequest {
282 content_cid: "QmTest".to_string(),
283 chunk_index: 0,
284 challenge_nonce: [1u8; 32],
285 requester_peer_id: "peer1".to_string(),
286 requester_public_key: keypair.public_key(),
287 timestamp_ms: chrono::Utc::now().timestamp_millis(),
288 };
289
290 assert!(validate_request_timestamp(&request, Duration::from_secs(300)).is_ok());
292
293 let old_request = ChunkRequest {
295 timestamp_ms: chrono::Utc::now().timestamp_millis() - 600_000, ..request
297 };
298 assert!(validate_request_timestamp(&old_request, Duration::from_secs(300)).is_err());
299 }
300
301 #[test]
302 fn test_calculate_expected_chunks() {
303 assert_eq!(calculate_expected_chunks(0), 1);
304 assert_eq!(calculate_expected_chunks(CHUNK_SIZE as u64), 1);
305 assert_eq!(calculate_expected_chunks(CHUNK_SIZE as u64 + 1), 2);
306 assert_eq!(calculate_expected_chunks(CHUNK_SIZE as u64 * 10), 10);
307 assert_eq!(calculate_expected_chunks(CHUNK_SIZE as u64 * 10 + 100), 11);
308 }
309
310 #[test]
311 fn test_validate_bandwidth() {
312 assert!(validate_bandwidth(100_000_000, 10_000, 10_000.0).is_ok());
314
315 assert!(validate_bandwidth(100_000_000, 0, 10_000.0).is_err());
317
318 assert!(validate_bandwidth(10_000_000_000, 1_000, 10_000.0).is_err());
320 }
321
322 #[test]
323 fn test_sanitize_cid() {
324 assert_eq!(sanitize_cid("QmTest123"), "QmTest123");
325 assert_eq!(sanitize_cid("Qm../../../etc/passwd"), "Qmetcpasswd");
326 assert_eq!(sanitize_cid("Qm Test@123!"), "QmTest123");
327 assert_eq!(sanitize_cid("valid-cid_123"), "valid-cid_123");
328 }
329
330 #[test]
331 fn test_validate_response_signature() {
332 let valid_response = ChunkResponse {
333 encrypted_chunk: vec![1u8; 100],
334 chunk_hash: [2u8; 32],
335 provider_signature: vec![3u8; 64],
336 provider_public_key: [4u8; 32],
337 challenge_echo: [5u8; 32],
338 timestamp_ms: chrono::Utc::now().timestamp_millis(),
339 };
340
341 assert!(validate_response_signature(&valid_response).is_ok());
342
343 let invalid_sig = ChunkResponse {
345 provider_signature: vec![3u8; 32],
346 ..valid_response.clone()
347 };
348 assert!(validate_response_signature(&invalid_sig).is_err());
349 }
350}