1use crate::errors::{Result, VisionError};
11use image::{DynamicImage, ImageFormat};
12use std::path::Path;
13use tracing::{debug, warn};
14
15pub const DEFAULT_MAX_FILE_SIZE: usize = 50 * 1024 * 1024;
17
18pub const DEFAULT_MAX_DIMENSION: u32 = 10_000;
20
21pub const DEFAULT_MIN_DIMENSION: u32 = 10;
23
24#[derive(Debug, Clone)]
26pub struct ValidationConfig {
27 pub max_file_size: usize,
29
30 pub max_dimension: u32,
32
33 pub min_dimension: u32,
35
36 pub allowed_formats: Vec<ImageFormat>,
38
39 pub deep_inspection: bool,
41
42 pub validate_aspect_ratio: bool,
44
45 pub max_aspect_ratio: f32,
47}
48
49impl Default for ValidationConfig {
50 fn default() -> Self {
51 Self {
52 max_file_size: DEFAULT_MAX_FILE_SIZE,
53 max_dimension: DEFAULT_MAX_DIMENSION,
54 min_dimension: DEFAULT_MIN_DIMENSION,
55 allowed_formats: vec![
56 ImageFormat::Png,
57 ImageFormat::Jpeg,
58 ImageFormat::WebP,
59 ImageFormat::Tiff,
60 ImageFormat::Bmp,
61 ],
62 deep_inspection: true,
63 validate_aspect_ratio: true,
64 max_aspect_ratio: 100.0, }
66 }
67}
68
69impl ValidationConfig {
70 pub fn permissive() -> Self {
72 Self {
73 max_file_size: usize::MAX,
74 max_dimension: u32::MAX,
75 min_dimension: 1,
76 allowed_formats: ImageFormat::all().collect(),
77 deep_inspection: false,
78 validate_aspect_ratio: false,
79 max_aspect_ratio: f32::MAX,
80 }
81 }
82
83 pub fn strict() -> Self {
85 Self {
86 max_file_size: 10 * 1024 * 1024, max_dimension: 5_000,
88 min_dimension: 50,
89 allowed_formats: vec![ImageFormat::Png, ImageFormat::Jpeg],
90 deep_inspection: true,
91 validate_aspect_ratio: true,
92 max_aspect_ratio: 20.0,
93 }
94 }
95}
96
97pub struct ImageValidator {
99 config: ValidationConfig,
100}
101
102impl ImageValidator {
103 pub fn new() -> Self {
105 Self {
106 config: ValidationConfig::default(),
107 }
108 }
109
110 pub fn with_config(config: ValidationConfig) -> Self {
112 Self { config }
113 }
114
115 pub fn validate_bytes(&self, data: &[u8]) -> Result<()> {
117 debug!("Validating image bytes (size: {} bytes)", data.len());
118
119 if data.len() > self.config.max_file_size {
121 warn!(
122 "Image size {} exceeds maximum allowed size {}",
123 data.len(),
124 self.config.max_file_size
125 );
126 return Err(VisionError::InvalidFormat(format!(
127 "Image size {} bytes exceeds maximum allowed {} bytes",
128 data.len(),
129 self.config.max_file_size
130 )));
131 }
132
133 if data.is_empty() {
135 return Err(VisionError::InvalidFormat(
136 "Image data is empty".to_string(),
137 ));
138 }
139
140 let format = self.detect_format(data)?;
142 self.validate_format(&format)?;
143
144 let image = image::load_from_memory(data)
146 .map_err(|e| VisionError::ImageDecode(format!("Failed to decode image: {}", e)))?;
147
148 self.validate_dimensions(&image)?;
150
151 if self.config.deep_inspection {
153 self.deep_inspect(&image)?;
154 }
155
156 debug!("Image validation passed");
157 Ok(())
158 }
159
160 pub fn validate_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
162 let path = path.as_ref();
163 debug!("Validating image file: {}", path.display());
164
165 if !path.exists() {
167 return Err(VisionError::InvalidFormat(format!(
168 "File not found: {}",
169 path.display()
170 )));
171 }
172
173 let metadata = std::fs::metadata(path).map_err(|e| {
175 VisionError::InvalidFormat(format!("Failed to read file metadata: {}", e))
176 })?;
177
178 if metadata.len() > self.config.max_file_size as u64 {
179 return Err(VisionError::InvalidFormat(format!(
180 "File size {} bytes exceeds maximum allowed {} bytes",
181 metadata.len(),
182 self.config.max_file_size
183 )));
184 }
185
186 if let Some(ext) = path.extension() {
188 let ext_str = ext.to_string_lossy().to_lowercase();
189 let valid_extensions = ["png", "jpg", "jpeg", "webp", "tiff", "tif", "bmp"];
190 if !valid_extensions.contains(&ext_str.as_str()) {
191 warn!("Suspicious file extension: {}", ext_str);
192 }
193 }
194
195 let data = std::fs::read(path)
197 .map_err(|e| VisionError::InvalidFormat(format!("Failed to read file: {}", e)))?;
198
199 self.validate_bytes(&data)
200 }
201
202 pub fn validate_image(&self, image: &DynamicImage) -> Result<()> {
204 debug!("Validating DynamicImage");
205 self.validate_dimensions(image)?;
206
207 if self.config.deep_inspection {
208 self.deep_inspect(image)?;
209 }
210
211 Ok(())
212 }
213
214 fn detect_format(&self, data: &[u8]) -> Result<ImageFormat> {
216 image::guess_format(data).map_err(|e| {
217 VisionError::InvalidFormat(format!("Failed to detect image format: {}", e))
218 })
219 }
220
221 fn validate_format(&self, format: &ImageFormat) -> Result<()> {
223 if !self.config.allowed_formats.contains(format) {
224 return Err(VisionError::InvalidFormat(format!(
225 "Image format {:?} is not allowed. Allowed formats: {:?}",
226 format, self.config.allowed_formats
227 )));
228 }
229 Ok(())
230 }
231
232 fn validate_dimensions(&self, image: &DynamicImage) -> Result<()> {
234 let (width, height) = (image.width(), image.height());
235
236 debug!("Image dimensions: {}x{}", width, height);
237
238 if width > self.config.max_dimension || height > self.config.max_dimension {
240 return Err(VisionError::InvalidFormat(format!(
241 "Image dimensions {}x{} exceed maximum allowed {}x{}",
242 width, height, self.config.max_dimension, self.config.max_dimension
243 )));
244 }
245
246 if width < self.config.min_dimension || height < self.config.min_dimension {
248 return Err(VisionError::InvalidFormat(format!(
249 "Image dimensions {}x{} are below minimum required {}x{}",
250 width, height, self.config.min_dimension, self.config.min_dimension
251 )));
252 }
253
254 if self.config.validate_aspect_ratio {
256 let aspect_ratio = width.max(height) as f32 / width.min(height) as f32;
257 if aspect_ratio > self.config.max_aspect_ratio {
258 warn!(
259 "Image aspect ratio {:.2} exceeds maximum {:.2}",
260 aspect_ratio, self.config.max_aspect_ratio
261 );
262 return Err(VisionError::InvalidFormat(format!(
263 "Image aspect ratio {:.2} exceeds maximum allowed {:.2}",
264 aspect_ratio, self.config.max_aspect_ratio
265 )));
266 }
267 }
268
269 Ok(())
270 }
271
272 fn deep_inspect(&self, image: &DynamicImage) -> Result<()> {
274 debug!("Performing deep content inspection");
275
276 self.check_entropy(image)?;
278 self.check_color_distribution(image)?;
279
280 Ok(())
281 }
282
283 fn check_entropy(&self, image: &DynamicImage) -> Result<()> {
285 let pixels = image.to_rgba8();
286 let (width, height) = pixels.dimensions();
287 let pixel_count = (width * height) as usize;
288
289 let mut color_samples = std::collections::HashSet::new();
292 let sample_size = pixel_count.min(1000);
293
294 for y in
295 (0..height).step_by((height as usize / (sample_size as f32).sqrt() as usize).max(1))
296 {
297 for x in
298 (0..width).step_by((width as usize / (sample_size as f32).sqrt() as usize).max(1))
299 {
300 let pixel = pixels.get_pixel(x, y);
301 let color = (pixel[0], pixel[1], pixel[2], pixel[3]);
302 color_samples.insert(color);
303 }
304 }
305
306 let uniqueness_ratio = color_samples.len() as f32 / sample_size as f32;
307
308 if uniqueness_ratio > 0.95 {
310 debug!(
311 "High entropy detected: uniqueness ratio = {:.2}",
312 uniqueness_ratio
313 );
314 }
317
318 Ok(())
319 }
320
321 fn check_color_distribution(&self, image: &DynamicImage) -> Result<()> {
323 let pixels = image.to_rgba8();
324
325 let mut all_transparent = true;
327 for chunk in pixels.chunks(4) {
328 if chunk[3] > 0 {
329 all_transparent = false;
331 break;
332 }
333 }
334
335 if all_transparent {
336 return Err(VisionError::InvalidFormat(
337 "Image is completely transparent".to_string(),
338 ));
339 }
340
341 Ok(())
342 }
343
344 pub fn config(&self) -> &ValidationConfig {
346 &self.config
347 }
348}
349
350impl Default for ImageValidator {
351 fn default() -> Self {
352 Self::new()
353 }
354}
355
356pub fn validate_image_bytes(data: &[u8]) -> Result<()> {
358 ImageValidator::new().validate_bytes(data)
359}
360
361pub fn validate_image_file<P: AsRef<Path>>(path: P) -> Result<()> {
363 ImageValidator::new().validate_file(path)
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use image::{ImageBuffer, Rgba};
370 use std::io::Cursor;
371
372 fn create_test_image(width: u32, height: u32) -> DynamicImage {
373 let img = ImageBuffer::from_fn(width, height, |x, y| {
374 Rgba([(x % 256) as u8, (y % 256) as u8, 128, 255])
375 });
376 DynamicImage::ImageRgba8(img)
377 }
378
379 fn image_to_bytes(image: &DynamicImage, format: ImageFormat) -> Vec<u8> {
380 let mut bytes = Vec::new();
381 let mut cursor = Cursor::new(&mut bytes);
382 image.write_to(&mut cursor, format).unwrap();
383 bytes
384 }
385
386 #[test]
387 fn test_validator_creation() {
388 let validator = ImageValidator::new();
389 assert_eq!(validator.config.max_file_size, DEFAULT_MAX_FILE_SIZE);
390 }
391
392 #[test]
393 fn test_validator_with_config() {
394 let config = ValidationConfig::strict();
395 let validator = ImageValidator::with_config(config.clone());
396 assert_eq!(validator.config.max_file_size, config.max_file_size);
397 }
398
399 #[test]
400 fn test_validate_dimensions_valid() {
401 let validator = ImageValidator::new();
402 let image = create_test_image(800, 600);
403 assert!(validator.validate_dimensions(&image).is_ok());
404 }
405
406 #[test]
407 fn test_validate_dimensions_too_large() {
408 let validator = ImageValidator::new();
409 let image = create_test_image(15000, 600);
410 assert!(validator.validate_dimensions(&image).is_err());
411 }
412
413 #[test]
414 fn test_validate_dimensions_too_small() {
415 let validator = ImageValidator::new();
416 let image = create_test_image(5, 5);
417 assert!(validator.validate_dimensions(&image).is_err());
418 }
419
420 #[test]
421 fn test_validate_aspect_ratio_valid() {
422 let config = ValidationConfig {
423 max_aspect_ratio: 10.0,
424 ..Default::default()
425 };
426 let validator = ImageValidator::with_config(config);
427 let image = create_test_image(1000, 100); assert!(validator.validate_dimensions(&image).is_ok());
429 }
430
431 #[test]
432 fn test_validate_aspect_ratio_invalid() {
433 let config = ValidationConfig {
434 max_aspect_ratio: 5.0,
435 ..Default::default()
436 };
437 let validator = ImageValidator::with_config(config);
438 let image = create_test_image(1000, 100); assert!(validator.validate_dimensions(&image).is_err());
440 }
441
442 #[test]
443 fn test_validate_bytes_valid_png() {
444 let image = create_test_image(800, 600);
445 let bytes = image_to_bytes(&image, ImageFormat::Png);
446 let validator = ImageValidator::new();
447 assert!(validator.validate_bytes(&bytes).is_ok());
448 }
449
450 #[test]
451 fn test_validate_bytes_empty() {
452 let validator = ImageValidator::new();
453 let result = validator.validate_bytes(&[]);
454 assert!(result.is_err());
455 }
456
457 #[test]
458 fn test_validate_bytes_too_large() {
459 let config = ValidationConfig {
460 max_file_size: 1000,
461 ..Default::default()
462 };
463 let validator = ImageValidator::with_config(config);
464 let image = create_test_image(800, 600);
465 let bytes = image_to_bytes(&image, ImageFormat::Png);
466 assert!(validator.validate_bytes(&bytes).is_err());
467 }
468
469 #[test]
470 fn test_validate_format_allowed() {
471 let validator = ImageValidator::new();
472 assert!(validator.validate_format(&ImageFormat::Png).is_ok());
473 assert!(validator.validate_format(&ImageFormat::Jpeg).is_ok());
474 }
475
476 #[test]
477 fn test_validate_format_not_allowed() {
478 let config = ValidationConfig {
479 allowed_formats: vec![ImageFormat::Png],
480 ..Default::default()
481 };
482 let validator = ImageValidator::with_config(config);
483 assert!(validator.validate_format(&ImageFormat::Jpeg).is_err());
484 }
485
486 #[test]
487 fn test_permissive_config() {
488 let config = ValidationConfig::permissive();
489 assert_eq!(config.max_file_size, usize::MAX);
490 assert_eq!(config.max_dimension, u32::MAX);
491 }
492
493 #[test]
494 fn test_strict_config() {
495 let config = ValidationConfig::strict();
496 assert_eq!(config.max_file_size, 10 * 1024 * 1024);
497 assert_eq!(config.max_dimension, 5_000);
498 }
499
500 #[test]
501 fn test_check_entropy() {
502 let validator = ImageValidator::new();
503 let image = create_test_image(800, 600);
504 assert!(validator.check_entropy(&image).is_ok());
505 }
506
507 #[test]
508 fn test_check_color_distribution_valid() {
509 let validator = ImageValidator::new();
510 let image = create_test_image(800, 600);
511 assert!(validator.check_color_distribution(&image).is_ok());
512 }
513
514 #[test]
515 fn test_check_color_distribution_transparent() {
516 let validator = ImageValidator::new();
517 let img = ImageBuffer::from_pixel(100, 100, Rgba([0, 0, 0, 0]));
518 let image = DynamicImage::ImageRgba8(img);
519 assert!(validator.check_color_distribution(&image).is_err());
520 }
521
522 #[test]
523 fn test_validate_image_bytes_helper() {
524 let image = create_test_image(800, 600);
525 let bytes = image_to_bytes(&image, ImageFormat::Png);
526 assert!(validate_image_bytes(&bytes).is_ok());
527 }
528
529 #[test]
530 fn test_deep_inspection() {
531 let validator = ImageValidator::new();
532 let image = create_test_image(800, 600);
533 assert!(validator.deep_inspect(&image).is_ok());
534 }
535
536 #[test]
537 fn test_validation_config_default() {
538 let config = ValidationConfig::default();
539 assert_eq!(config.max_file_size, DEFAULT_MAX_FILE_SIZE);
540 assert_eq!(config.max_dimension, DEFAULT_MAX_DIMENSION);
541 assert!(config.deep_inspection);
542 }
543}