1use crate::error::DecodeError;
9
10const SOI_SCAN_LIMIT: usize = 1024;
12
13const SOF_MARKER_BYTE_OFFSET: usize = 5;
18
19const KNOWN_BAD_HEADER_OFFSETS: &[usize] = &[94, 163];
24
25#[derive(Debug, Clone, Default)]
27pub struct JpegInfo {
28 pub width: u32,
30 pub height: u32,
32 pub num_components: u8,
34 pub bits_per_component: u8,
36 pub has_adobe_marker: bool,
38 pub adobe_color_transform: u8,
40}
41
42pub fn probe_jpeg_info(input: &[u8]) -> Option<JpegInfo> {
48 let data = find_soi(input)?;
49 let mut info = JpegInfo::default();
50 let mut found_sof = false;
51 let mut pos = 2; while pos + 1 < data.len() {
54 if data[pos] != 0xFF {
55 pos += 1;
56 continue;
57 }
58
59 while pos + 1 < data.len() && data[pos + 1] == 0xFF {
61 pos += 1;
62 }
63 if pos + 1 >= data.len() {
64 break;
65 }
66
67 let marker = data[pos + 1];
68 pos += 2;
69
70 if marker == 0xDA {
72 break;
73 }
74
75 if marker == 0x00 || marker == 0x01 || (0xD0..=0xD7).contains(&marker) {
77 continue;
78 }
79
80 if pos + 2 > data.len() {
82 break;
83 }
84 let length = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
85 if length < 2 || pos + length > data.len() {
86 break;
87 }
88
89 match marker {
90 0xC0..=0xC2 => {
92 if length >= 8 {
93 info.bits_per_component = data[pos + 2];
94 info.height = u16::from_be_bytes([data[pos + 3], data[pos + 4]]) as u32;
95 info.width = u16::from_be_bytes([data[pos + 5], data[pos + 6]]) as u32;
96 info.num_components = data[pos + 7];
97 found_sof = true;
98 }
99 }
100 0xEE => {
102 if length >= 14 && pos + 2 + 5 <= data.len() && &data[pos + 2..pos + 7] == b"Adobe"
104 {
105 info.has_adobe_marker = true;
106 if pos + 13 < data.len() {
109 info.adobe_color_transform = data[pos + 13];
110 }
111 }
112 }
113 _ => {}
114 }
115
116 pos += length;
117 }
118
119 if found_sof { Some(info) } else { None }
120}
121
122#[inline]
124pub fn load_info(input: &[u8]) -> Option<JpegInfo> {
125 probe_jpeg_info(input)
126}
127
128fn find_soi(input: &[u8]) -> Option<&[u8]> {
133 if input.len() < 2 {
134 return None;
135 }
136
137 if input[0] == 0xFF && input[1] == 0xD8 {
138 return Some(input);
139 }
140
141 let limit = input.len().min(SOI_SCAN_LIMIT);
143 for i in 0..limit.saturating_sub(1) {
144 if input[i] == 0xFF && input[i + 1] == 0xD8 {
145 return Some(&input[i..]);
146 }
147 }
148
149 None
150}
151
152fn is_sof_at_offset(data: &[u8], dimension_offset: usize) -> bool {
156 if dimension_offset < SOF_MARKER_BYTE_OFFSET {
157 return false;
158 }
159 let marker_offset = dimension_offset - SOF_MARKER_BYTE_OFFSET;
160 if marker_offset + 1 >= data.len() {
161 return false;
162 }
163 data[marker_offset] == 0xFF && (0xC0..=0xC2).contains(&data[marker_offset + 1])
164}
165
166fn has_invalid_height_at_offset(data: &[u8], dimension_offset: usize, expected_width: u32) -> bool {
171 if dimension_offset + 3 >= data.len() {
172 return false;
173 }
174 if !is_sof_at_offset(data, dimension_offset) {
175 return false;
176 }
177
178 let width_hi = ((expected_width >> 8) & 0xFF) as u8;
179 let width_lo = (expected_width & 0xFF) as u8;
180
181 data[dimension_offset] == 0xFF
183 && data[dimension_offset + 1] == 0xFF
184 && data[dimension_offset + 2] == width_hi
185 && data[dimension_offset + 3] == width_lo
186}
187
188pub fn try_patch_invalid_height(input: &[u8]) -> Option<Vec<u8>> {
197 let data = find_soi(input)?;
198 let soi_offset = input.len() - data.len();
199
200 let info = probe_jpeg_info(data)?;
201 if info.height != 0xFFFF {
202 return None;
203 }
204
205 for &offset in KNOWN_BAD_HEADER_OFFSETS {
206 if has_invalid_height_at_offset(data, offset, info.width as u32) {
207 let mut patched = input.to_vec();
208 let abs_offset = soi_offset + offset;
209 if abs_offset + 1 < patched.len() {
210 patched[abs_offset] = 0x00;
212 patched[abs_offset + 1] = 0x00;
213 return Some(patched);
214 }
215 }
216 }
217
218 None
219}
220
221pub fn decode_with_recovery(input: &[u8]) -> Result<Vec<u8>, DecodeError> {
227 match decode(input) {
228 Ok(data) => Ok(data),
229 Err(original_err) => {
230 if let Some(patched) = try_patch_invalid_height(input) {
231 decode(&patched)
232 } else {
233 Err(original_err)
234 }
235 }
236 }
237}
238
239pub fn decode(input: &[u8]) -> Result<Vec<u8>, DecodeError> {
245 let data = find_soi(input).unwrap_or(input);
246 let cursor = std::io::Cursor::new(data);
247 let mut decoder = zune_jpeg::JpegDecoder::new(cursor);
248 decoder
249 .decode()
250 .map_err(|e| DecodeError::Jpeg(format!("{e}")))
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_decode_invalid_jpeg() {
259 let result = decode(b"not a jpeg");
260 assert!(result.is_err());
261 }
262
263 #[test]
264 fn test_decode_empty_input() {
265 let result = decode(b"");
266 assert!(result.is_err());
267 }
268
269 #[test]
270 fn test_decode_truncated_header() {
271 let result = decode(&[0xFF, 0xD8, 0xFF]);
273 assert!(result.is_err());
274 }
275
276 #[test]
277 fn test_decode_minimal_valid_jpeg() {
278 let partial_jpeg = [
281 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, b'J', b'F', b'I', b'F', 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, ];
291 let result = decode(&partial_jpeg);
292 assert!(result.is_err());
294 }
295
296 fn build_jpeg_with_sof(width: u16, height: u16, components: u8, bpc: u8) -> Vec<u8> {
298 let mut data = Vec::new();
299 data.extend_from_slice(&[0xFF, 0xD8]);
301 data.extend_from_slice(&[0xFF, 0xC0]);
303 let sof_len = 8 + 3 * components as u16;
305 data.extend_from_slice(&sof_len.to_be_bytes());
306 data.push(bpc);
307 data.extend_from_slice(&height.to_be_bytes());
308 data.extend_from_slice(&width.to_be_bytes());
309 data.push(components);
310 for i in 0..components {
312 data.extend_from_slice(&[i + 1, 0x11, 0]);
313 }
314 data
315 }
316
317 #[test]
318 fn test_probe_jpeg_info_basic() {
319 let jpeg = build_jpeg_with_sof(640, 480, 3, 8);
320 let info = probe_jpeg_info(&jpeg).unwrap();
321 assert_eq!(info.width, 640);
322 assert_eq!(info.height, 480);
323 assert_eq!(info.num_components, 3);
324 assert_eq!(info.bits_per_component, 8);
325 assert!(!info.has_adobe_marker);
326 }
327
328 #[test]
329 fn test_probe_jpeg_info_grayscale() {
330 let jpeg = build_jpeg_with_sof(100, 200, 1, 8);
331 let info = probe_jpeg_info(&jpeg).unwrap();
332 assert_eq!(info.width, 100);
333 assert_eq!(info.height, 200);
334 assert_eq!(info.num_components, 1);
335 }
336
337 #[test]
338 fn test_probe_jpeg_info_with_adobe_marker() {
339 let mut jpeg = build_jpeg_with_sof(320, 240, 4, 8);
340 jpeg.extend_from_slice(&[0xFF, 0xEE]);
342 jpeg.extend_from_slice(&[0x00, 0x0E]);
344 jpeg.extend_from_slice(b"Adobe");
346 jpeg.extend_from_slice(&[0x00, 0x64]);
348 jpeg.extend_from_slice(&[0x00, 0x00]);
350 jpeg.extend_from_slice(&[0x00, 0x00]);
352 jpeg.push(0x02);
354
355 let info = probe_jpeg_info(&jpeg).unwrap();
356 assert_eq!(info.width, 320);
357 assert_eq!(info.num_components, 4);
358 assert!(info.has_adobe_marker);
359 assert_eq!(info.adobe_color_transform, 2);
360 }
361
362 #[test]
363 fn test_probe_jpeg_info_no_sof_returns_none() {
364 let data = [0xFF, 0xD8];
366 assert!(probe_jpeg_info(&data).is_none());
367 }
368
369 #[test]
370 fn test_probe_jpeg_info_empty_returns_none() {
371 assert!(probe_jpeg_info(&[]).is_none());
372 }
373
374 #[test]
375 fn test_find_soi_at_start() {
376 let data = [0xFF, 0xD8, 0xFF, 0xC0];
377 let result = find_soi(&data);
378 assert!(result.is_some());
379 assert_eq!(result.unwrap().len(), 4);
380 }
381
382 #[test]
383 fn test_find_soi_with_offset() {
384 let mut data = vec![0x00, 0x00, 0x00, 0x42];
386 data.extend_from_slice(&[0xFF, 0xD8, 0xFF, 0xC0]);
387 let result = find_soi(&data);
388 assert!(result.is_some());
389 assert_eq!(result.unwrap().len(), 4); }
391
392 #[test]
393 fn test_find_soi_not_found() {
394 let data = [0x00; 100];
395 assert!(find_soi(&data).is_none());
396 }
397
398 #[test]
399 fn test_find_soi_beyond_limit() {
400 let mut data = vec![0x00; SOI_SCAN_LIMIT + 10];
402 data[SOI_SCAN_LIMIT + 5] = 0xFF;
403 data[SOI_SCAN_LIMIT + 6] = 0xD8;
404 assert!(find_soi(&data).is_none());
405 }
406
407 #[test]
408 fn test_decode_with_leading_garbage_still_fails_gracefully() {
409 let mut data = vec![0x00, 0x00, 0x00];
411 data.extend_from_slice(&[0xFF, 0xD8, 0xFF]);
412 let result = decode(&data);
413 assert!(result.is_err());
414 }
415
416 fn build_jpeg_with_sof_at_offset(
422 offset: usize,
423 width: u16,
424 height: u16,
425 components: u8,
426 ) -> Vec<u8> {
427 let mut data = Vec::new();
428 data.extend_from_slice(&[0xFF, 0xD8]);
430
431 let sof_marker_pos = offset - SOF_MARKER_BYTE_OFFSET;
434 let current_pos = 2; if sof_marker_pos > current_pos {
436 let fill_len = sof_marker_pos - current_pos;
438 if fill_len >= 4 {
439 data.extend_from_slice(&[0xFF, 0xE0]); let seg_len = (fill_len - 2) as u16;
441 data.extend_from_slice(&seg_len.to_be_bytes());
442 data.extend(vec![0x00; fill_len - 4]);
443 } else {
444 data.extend(vec![0x00; fill_len]);
445 }
446 }
447
448 data.extend_from_slice(&[0xFF, 0xC0]);
450 let sof_len = 8 + 3 * components as u16;
451 data.extend_from_slice(&sof_len.to_be_bytes());
452 data.push(8); data.extend_from_slice(&height.to_be_bytes());
454 data.extend_from_slice(&width.to_be_bytes());
455 data.push(components);
456 for i in 0..components {
457 data.extend_from_slice(&[i + 1, 0x11, 0]);
458 }
459 data
460 }
461
462 #[test]
463 fn test_detect_known_bad_header_at_offset_94() {
464 let data = build_jpeg_with_sof_at_offset(94, 640, 0xFFFF, 3);
465 let info = probe_jpeg_info(&data).unwrap();
466 assert_eq!(info.height, 0xFFFF);
467 assert_eq!(info.width, 640);
468
469 let patched = try_patch_invalid_height(&data);
470 assert!(patched.is_some());
471
472 let patched = patched.unwrap();
474 let patched_info = probe_jpeg_info(&patched).unwrap();
475 assert_eq!(patched_info.height, 0);
476 assert_eq!(patched_info.width, 640);
477 }
478
479 #[test]
480 fn test_detect_known_bad_header_at_offset_163() {
481 let data = build_jpeg_with_sof_at_offset(163, 320, 0xFFFF, 3);
482 let info = probe_jpeg_info(&data).unwrap();
483 assert_eq!(info.height, 0xFFFF);
484
485 let patched = try_patch_invalid_height(&data);
486 assert!(patched.is_some());
487 let patched_info = probe_jpeg_info(&patched.unwrap()).unwrap();
488 assert_eq!(patched_info.height, 0);
489 }
490
491 #[test]
492 fn test_no_patch_for_normal_height() {
493 let data = build_jpeg_with_sof(640, 480, 3, 8);
494 assert!(try_patch_invalid_height(&data).is_none());
495 }
496
497 #[test]
498 fn test_no_patch_for_unknown_offset() {
499 let data = build_jpeg_with_sof_at_offset(50, 640, 0xFFFF, 3);
501 let info = probe_jpeg_info(&data).unwrap();
502 assert_eq!(info.height, 0xFFFF);
503 assert!(try_patch_invalid_height(&data).is_none());
505 }
506
507 #[test]
508 fn test_decode_with_recovery_on_invalid_input() {
509 let result = decode_with_recovery(b"not a jpeg");
511 assert!(result.is_err());
512 }
513
514 #[test]
515 fn test_is_sof_at_offset_basic() {
516 let data = [0xFF, 0xC0, 0x00, 0x08, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01];
518 assert!(is_sof_at_offset(&data, SOF_MARKER_BYTE_OFFSET));
519 }
520
521 #[test]
522 fn test_is_sof_at_offset_sof2() {
523 let data = [0xFF, 0xC2, 0x00, 0x08, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01];
525 assert!(is_sof_at_offset(&data, SOF_MARKER_BYTE_OFFSET));
526 }
527
528 #[test]
529 fn test_is_sof_at_offset_not_sof() {
530 let data = [0xFF, 0xE0, 0x00, 0x08, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01];
532 assert!(!is_sof_at_offset(&data, SOF_MARKER_BYTE_OFFSET));
533 }
534}