kanidmd_lib/valueset/image/
jpg.rs

1use std::io::Cursor;
2
3use image::codecs::jpeg::JpegDecoder;
4use image::ImageDecoder;
5use sketching::*;
6
7use super::ImageValidationError;
8
9const JPEG_MAGIC: [u8; 2] = [0xff, 0xd8];
10const EOI_MAGIC: [u8; 2] = [0xff, 0xd9];
11const SOS_MARKER: [u8; 2] = [0xff, 0xda];
12
13/// Checks to see if it has a valid JPEG magic bytes header
14pub fn check_jpg_header(contents: &[u8]) -> Result<(), ImageValidationError> {
15    if !contents.starts_with(&JPEG_MAGIC) {
16        return Err(ImageValidationError::InvalidImage(
17            "Failed to parse JPEG file, invalid magic bytes".to_string(),
18        ));
19    }
20    Ok(())
21}
22
23// It's public so we can use it in benchmarking
24/// Check to see if JPG is affected by acropalypse issues, returns `Ok(true)` if it is
25/// based on <https://github.com/lordofpipes/acropadetect/blob/main/src/detect.ts>
26pub fn has_trailer(contents: &Vec<u8>) -> Result<bool, ImageValidationError> {
27    let buf = contents.as_slice();
28
29    let mut pos = JPEG_MAGIC.len();
30
31    while pos < buf.len() {
32        let marker = &buf[pos..pos + 2];
33        pos += 2;
34
35        let segment_size_bytes: &[u8] = &buf[pos..pos + 2];
36        let segment_size = u16::from_be_bytes(segment_size_bytes.try_into().map_err(|_| {
37            ImageValidationError::InvalidImage("JPEG segment size bytes were invalid!".to_string())
38        })?);
39        // we do not add 2 because the size prefix includes the size of the size prefix
40        pos += segment_size as usize;
41
42        if marker == SOS_MARKER {
43            break;
44        }
45    }
46
47    // setting this to a big value so we can see if we don't find the EOI marker
48    let mut eoi_index = buf.len() * 2;
49    trace!("buffer length: {}", buf.len());
50
51    // iterate through the file looking for the EOI_MAGIC bytes
52    for i in pos..=(buf.len() - EOI_MAGIC.len()) {
53        if buf[i..(i + EOI_MAGIC.len())] == EOI_MAGIC {
54            eoi_index = i;
55            break;
56        }
57    }
58
59    if eoi_index > buf.len() {
60        Err(ImageValidationError::InvalidImage(
61            "End of image magic bytes not found in JPEG".to_string(),
62        ))
63    } else if (eoi_index + 2) < buf.len() {
64        // there's still bytes in the buffer after the EOI magic bytes
65        debug!(
66            "we're at pos: {} and buf len is {}, is not OK",
67            eoi_index,
68            buf.len()
69        );
70        Ok(true)
71    } else {
72        debug!(
73            "we're at pos: {} and buf len is {}, is OK",
74            eoi_index,
75            buf.len()
76        );
77        Ok(false)
78    }
79}
80
81pub fn validate_decoding(
82    filename: &str,
83    contents: &[u8],
84    limits: image::Limits,
85) -> Result<(), ImageValidationError> {
86    let mut decoder = match JpegDecoder::new(Cursor::new(contents)) {
87        Ok(val) => val,
88        Err(err) => {
89            return Err(ImageValidationError::InvalidImage(format!(
90                "Failed to parse {} as JPG: {:?}",
91                filename, err
92            )))
93        }
94    };
95
96    match decoder.set_limits(limits) {
97        Err(err) => {
98            sketching::admin_warn!(
99                "Image validation failed while validating {}: {:?}",
100                filename,
101                err
102            );
103            Err(ImageValidationError::ExceedsMaxDimensions)
104        }
105        Ok(_) => Ok(()),
106    }
107}
108
109#[test]
110fn test_jpg_has_trailer() {
111    let file_contents = std::fs::read(format!(
112        "{}/src/valueset/image/test_images/oversize_dimensions.jpg",
113        env!("CARGO_MANIFEST_DIR")
114    ))
115    .expect("Failed to read file");
116    assert!(!has_trailer(&file_contents).expect("Failed to check for JPEG trailer"));
117
118    // checking a known bad image
119    let file_contents = std::fs::read(format!(
120        "{}/src/valueset/image/test_images/windows11_3_cropped.jpg",
121        env!("CARGO_MANIFEST_DIR")
122    ))
123    .expect("Failed to read file");
124    // let test_bytes = vec![0xff, 0xd8, 0xff, 0xda, 0xff, 0xd9];
125
126    assert!(has_trailer(&file_contents).expect("Failed to check for JPEG trailer"));
127}