1use base64::{engine::general_purpose::STANDARD, Engine};
9
10pub fn detect_supported_image_mime_type(bytes: &[u8]) -> Option<&'static str> {
15 if is_jpeg(bytes) {
16 return Some("image/jpeg");
17 }
18 if is_png(bytes) {
19 return Some("image/png");
20 }
21 if starts_with_ascii_at(bytes, 0, "GIF") && bytes.len() >= 6 {
22 return Some("image/gif");
24 }
25 if starts_with_ascii_at(bytes, 0, "RIFF") && starts_with_ascii_at(bytes, 8, "WEBP") {
26 return Some("image/webp");
27 }
28 if starts_with_ascii_at(bytes, 0, "BM") && is_bmp(bytes) {
29 return Some("image/bmp");
30 }
31 None
32}
33
34pub fn encode_base64(bytes: &[u8]) -> String {
37 STANDARD.encode(bytes)
38}
39
40fn read_u16_le(bytes: &[u8], offset: usize) -> u16 {
45 if offset + 1 >= bytes.len() {
46 return 0;
47 }
48 u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
49}
50
51fn read_u32_be(bytes: &[u8], offset: usize) -> u32 {
52 if offset + 3 >= bytes.len() {
53 return 0;
54 }
55 u32::from_be_bytes([
56 bytes[offset],
57 bytes[offset + 1],
58 bytes[offset + 2],
59 bytes[offset + 3],
60 ])
61}
62
63fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
64 if offset + 3 >= bytes.len() {
65 return 0;
66 }
67 u32::from_le_bytes([
68 bytes[offset],
69 bytes[offset + 1],
70 bytes[offset + 2],
71 bytes[offset + 3],
72 ])
73}
74
75fn starts_with(bytes: &[u8], prefix: &[u8]) -> bool {
76 bytes.len() >= prefix.len() && &bytes[..prefix.len()] == prefix
77}
78
79fn starts_with_ascii_at(bytes: &[u8], offset: usize, ascii: &str) -> bool {
80 let pat = ascii.as_bytes();
81 if offset + pat.len() > bytes.len() {
82 return false;
83 }
84 &bytes[offset..offset + pat.len()] == pat
85}
86
87fn is_jpeg(bytes: &[u8]) -> bool {
88 starts_with(bytes, &[0xFF, 0xD8, 0xFF]) && bytes.get(3).copied() != Some(0xF7)
90}
91
92const PNG_SIGNATURE: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
93
94fn is_png(bytes: &[u8]) -> bool {
95 if !starts_with(bytes, &PNG_SIGNATURE) {
96 return false;
97 }
98 bytes.len() >= 16
100 && read_u32_be(bytes, 8) == 13
101 && starts_with_ascii_at(bytes, 12, "IHDR")
102 && !is_animated_png(bytes)
103}
104
105fn is_animated_png(bytes: &[u8]) -> bool {
108 if bytes.len() < 8 {
109 return false;
110 }
111 let mut offset = 8usize;
112 loop {
113 if offset + 8 > bytes.len() {
115 return false;
116 }
117 let length = read_u32_be(bytes, offset) as usize;
118 let type_offset = offset + 4;
119 if type_offset + 4 > bytes.len() {
120 return false;
121 }
122 let chunk_type = &bytes[type_offset..type_offset + 4];
123 if chunk_type == b"acTL" {
124 return true;
125 }
126 if chunk_type == b"IDAT" {
127 return false;
128 }
129 let next = offset + 4 + 4 + length + 4;
131 if next <= offset {
132 return false;
134 }
135 offset = next;
136 }
137}
138
139fn is_bmp(bytes: &[u8]) -> bool {
140 if bytes.len() < 26 {
141 return false;
142 }
143 let declared_file_size = read_u32_le(bytes, 2);
144 let pixel_data_offset = read_u32_le(bytes, 10) as usize;
145 let dib_header_size = read_u32_le(bytes, 14) as usize;
146
147 if declared_file_size != 0 && declared_file_size < 26 {
148 return false;
149 }
150 if pixel_data_offset < 14 + dib_header_size {
151 return false;
152 }
153 if declared_file_size != 0 && pixel_data_offset >= declared_file_size as usize {
154 return false;
155 }
156
157 let (planes, bpp) = if dib_header_size == 12 {
158 (read_u16_le(bytes, 22), read_u16_le(bytes, 24))
160 } else if (12..=124).contains(&dib_header_size) {
161 if bytes.len() < 30 {
162 return false;
163 }
164 (read_u16_le(bytes, 26), read_u16_le(bytes, 28))
165 } else {
166 return false;
167 };
168
169 planes == 1 && matches!(bpp, 1 | 4 | 8 | 16 | 24 | 32)
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn detects_jpeg() {
178 let bytes = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x00];
179 assert_eq!(detect_supported_image_mime_type(&bytes), Some("image/jpeg"));
180 }
181
182 #[test]
183 fn rejects_jpeg_xt_variant() {
184 let bytes = [0xFF, 0xD8, 0xFF, 0xF7, 0x00, 0x00];
186 assert_eq!(detect_supported_image_mime_type(&bytes), None);
187 }
188
189 #[test]
190 fn detects_png_static() {
191 let mut bytes = PNG_SIGNATURE.to_vec();
194 bytes.extend_from_slice(&13u32.to_be_bytes());
196 bytes.extend_from_slice(b"IHDR");
197 bytes.extend_from_slice(&[0u8; 13]);
198 bytes.extend_from_slice(&[0u8; 4]); bytes.extend_from_slice(&0u32.to_be_bytes());
201 bytes.extend_from_slice(b"IDAT");
202 bytes.extend_from_slice(&[0u8; 4]); assert_eq!(detect_supported_image_mime_type(&bytes), Some("image/png"));
204 }
205
206 #[test]
207 fn detects_gif_webp() {
208 let gif = b"GIF89a";
209 assert_eq!(detect_supported_image_mime_type(gif), Some("image/gif"));
210 let mut webp = b"RIFF????WEBP".to_vec();
211 let _ = &mut webp;
214 assert_eq!(detect_supported_image_mime_type(&webp), Some("image/webp"));
215 }
216
217 #[test]
218 fn rejects_empty_and_unknown() {
219 assert_eq!(detect_supported_image_mime_type(&[]), None);
220 assert_eq!(detect_supported_image_mime_type(b"hello world"), None);
221 }
222
223 #[test]
224 fn base64_no_newline() {
225 let s = encode_base64(b"hello");
226 assert!(!s.contains('\n'));
227 assert_eq!(s, "aGVsbG8=");
229 }
230
231 #[test]
232 fn base64_round_trip_empty() {
233 assert_eq!(encode_base64(&[]), "");
234 }
235}