megacommerce_shared/models/
images.rs

1use std::io::Cursor;
2
3use base64::{engine::general_purpose, Engine as _};
4use image::{ImageFormat, ImageReader};
5
6#[derive(Debug, Clone)]
7pub struct ImageValidationConfig {
8  pub max_size_bytes: usize,
9  pub allowed_formats: Vec<ImageFormat>,
10  pub max_width: u32,
11  pub max_height: u32,
12  pub min_width: u32,
13  pub min_height: u32,
14}
15
16#[derive(Debug, Clone)]
17pub struct ImageValidationResult {
18  pub format: ImageFormat,
19  pub dimensions: (u32, u32),
20  pub size_bytes: usize,
21  pub is_valid: bool,
22  pub decoded_data: Vec<u8>,
23}
24
25impl Default for ImageValidationConfig {
26  fn default() -> Self {
27    Self {
28      max_size_bytes: 1024 * 1024, // 1MB
29      allowed_formats: vec![ImageFormat::Png, ImageFormat::Jpeg, ImageFormat::WebP],
30      max_width: 4000,
31      max_height: 4000,
32      min_width: 100,
33      min_height: 100,
34    }
35  }
36}
37
38#[derive(Debug)]
39pub enum ImageValidationError {
40  LargeImage(String),
41  InvalidBase64(String),
42  UnknownFormat(String),
43  NotAllowedFormat(String),
44  SmallDimensions(String),
45  LargeDimensions(String),
46  UnknownDimensions(String),
47}
48
49pub fn validate_base64_image(
50  data: &str,
51  config: &ImageValidationConfig,
52) -> Result<ImageValidationResult, ImageValidationError> {
53  let clean_data = data.split(',').last().unwrap_or(data);
54
55  // Quick size check before decoding
56  // Base64 encoding uses 4 ASCII characters to represent 3 bytes of binary data:
57  // Input: 3 bytes of binary data (3 × 8 = 24 bits)
58  // Output: 4 base64 characters (4 × 6 = 24 bits)
59  // So the relationship is: 4 chars (base64) = 3 bytes (binary)
60  let estimated_size = (clean_data.len() * 3) / 4;
61  if estimated_size > config.max_size_bytes {
62    return Err(ImageValidationError::LargeImage(format!(
63      "Estimated size: {} is bigger than allowed size: {}",
64      estimated_size, config.max_size_bytes
65    )));
66  }
67
68  let decoded_data = general_purpose::STANDARD
69    .decode(clean_data)
70    .map_err(|err| ImageValidationError::InvalidBase64(format!("Invalid base64 data: {}", err)))?;
71
72  // Exact size validation
73  if decoded_data.len() > config.max_size_bytes {
74    return Err(ImageValidationError::LargeImage(format!(
75      "Actual image size: {} is bigger than allowed size: {}",
76      decoded_data.len(),
77      config.max_size_bytes
78    )));
79  }
80
81  let cursor = Cursor::new(&decoded_data);
82  let reader = ImageReader::new(cursor).with_guessed_format().map_err(|err| {
83    ImageValidationError::UnknownFormat(format!("Unable to determine image format: {}", err))
84  })?;
85
86  let format = reader.format().ok_or_else(|| {
87    ImageValidationError::UnknownFormat("Unable to determine image format".to_string())
88  })?;
89
90  if !config.allowed_formats.contains(&format) {
91    return Err(ImageValidationError::NotAllowedFormat(format!(
92      "Image format {} is not allowed",
93      format.extensions_str().first().unwrap_or(&"unknown")
94    )));
95  }
96
97  let dimensions = reader.into_dimensions().map_err(|err| {
98    ImageValidationError::UnknownDimensions(format!("Unable to get dimensions: {}", err))
99  })?;
100
101  if dimensions.0 < config.min_width || dimensions.1 < config.min_height {
102    return Err(ImageValidationError::SmallDimensions(format!(
103      "Image dimensions {}x{} are too small, minimum is {}x{}",
104      dimensions.0, dimensions.1, config.min_width, config.min_height
105    )));
106  }
107
108  if dimensions.0 > config.max_width || dimensions.1 > config.max_height {
109    return Err(ImageValidationError::LargeDimensions(format!(
110      "Image dimensions {}x{} are too large, maximum is {}x{}",
111      dimensions.0, dimensions.1, config.max_width, config.max_height
112    )));
113  }
114
115  Ok(ImageValidationResult {
116    format,
117    dimensions,
118    size_bytes: decoded_data.len(),
119    is_valid: true,
120    decoded_data,
121  })
122}