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() && pos + 4 < buf.len() {
32        // We assert pos and pos+4 are both in bounds during
33        // while iteration.
34        #[allow(clippy::indexing_slicing)]
35        let marker = &buf[pos..pos + 2];
36        pos += 2;
37
38        // pos was shifted by 2, so this is relative to +4 from
39        // where the loop started.
40        #[allow(clippy::indexing_slicing)]
41        let segment_size_bytes: &[u8] = &buf[pos..pos + 2];
42        let segment_size = u16::from_be_bytes(segment_size_bytes.try_into().map_err(|_| {
43            ImageValidationError::InvalidImage("JPEG segment size bytes were invalid!".to_string())
44        })?);
45        // we do not add 2 because the size prefix includes the size of the size prefix
46        pos += segment_size as usize;
47
48        if marker == SOS_MARKER {
49            break;
50        }
51    }
52
53    let mut eoi_index = 0;
54    let mut eoi_index_found = false;
55    trace!("buffer length: {}", buf.len());
56
57    // iterate through the file looking for the EOI_MAGIC bytes
58
59    let buf_limit = buf.len() - EOI_MAGIC.len();
60
61    for i in pos..=buf_limit {
62        if buf.get(i..(i + EOI_MAGIC.len())) == Some(&EOI_MAGIC) {
63            eoi_index_found = true;
64            eoi_index = i;
65            break;
66        }
67    }
68
69    if !eoi_index_found {
70        Err(ImageValidationError::InvalidImage(
71            "End of image magic bytes not found in JPEG".to_string(),
72        ))
73    } else if (eoi_index + 2) < buf.len() {
74        // there's still bytes in the buffer after the EOI magic bytes
75        debug!(
76            "we're at pos: {} and buf len is {}, is not OK",
77            eoi_index,
78            buf.len()
79        );
80        Ok(true)
81    } else {
82        debug!(
83            "we're at pos: {} and buf len is {}, is OK",
84            eoi_index,
85            buf.len()
86        );
87        Ok(false)
88    }
89}
90
91pub fn validate_decoding(
92    filename: &str,
93    contents: &[u8],
94    limits: image::Limits,
95) -> Result<(), ImageValidationError> {
96    let mut decoder = match JpegDecoder::new(Cursor::new(contents)) {
97        Ok(val) => val,
98        Err(err) => {
99            return Err(ImageValidationError::InvalidImage(format!(
100                "Failed to parse {filename} as JPG: {err:?}"
101            )))
102        }
103    };
104
105    match decoder.set_limits(limits) {
106        Err(err) => {
107            sketching::admin_warn!(
108                "Image validation failed while validating {}: {:?}",
109                filename,
110                err
111            );
112            Err(ImageValidationError::ExceedsMaxDimensions)
113        }
114        Ok(_) => Ok(()),
115    }
116}
117
118#[test]
119fn test_jpg_has_trailer() {
120    let file_contents = std::fs::read(format!(
121        "{}/src/valueset/image/test_images/oversize_dimensions.jpg",
122        env!("CARGO_MANIFEST_DIR")
123    ))
124    .expect("Failed to read file");
125    assert!(!has_trailer(&file_contents).expect("Failed to check for JPEG trailer"));
126
127    // checking a known bad image
128    let file_contents = std::fs::read(format!(
129        "{}/src/valueset/image/test_images/windows11_3_cropped.jpg",
130        env!("CARGO_MANIFEST_DIR")
131    ))
132    .expect("Failed to read file");
133    // let test_bytes = vec![0xff, 0xd8, 0xff, 0xda, 0xff, 0xd9];
134
135    assert!(has_trailer(&file_contents).expect("Failed to check for JPEG trailer"));
136}