kanidmd_lib/valueset/image/
jpg.rs1use 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
13pub 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
23pub 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 #[allow(clippy::indexing_slicing)]
35 let marker = &buf[pos..pos + 2];
36 pos += 2;
37
38 #[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 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 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 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 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 assert!(has_trailer(&file_contents).expect("Failed to check for JPEG trailer"));
136}