1#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct EmbeddedJpeg {
12 pub start: usize,
14 pub end: usize,
15 pub width: u16,
16 pub height: u16,
17 pub has_exif: bool,
19}
20
21impl EmbeddedJpeg {
22 pub fn pixels(&self) -> u64 {
23 self.width as u64 * self.height as u64
24 }
25
26 pub fn long_edge(&self) -> u16 {
27 self.width.max(self.height)
28 }
29}
30
31pub fn find_jpegs(data: &[u8]) -> Vec<EmbeddedJpeg> {
35 let mut found = Vec::new();
36 let mut i = 0;
37 while i + 3 < data.len() {
38 let Some(offset) = data[i..].windows(3).position(|w| w == [0xFF, 0xD8, 0xFF]) else {
39 break;
40 };
41 let start = i + offset;
42 match parse_jpeg(data, start) {
43 Some(jpeg) => {
44 i = jpeg.end;
45 found.push(jpeg);
46 }
47 None => i = start + 1,
48 }
49 }
50 found
51}
52
53fn u16_be(data: &[u8], at: usize) -> Option<u16> {
54 Some(u16::from_be_bytes([*data.get(at)?, *data.get(at + 1)?]))
55}
56
57fn parse_jpeg(data: &[u8], start: usize) -> Option<EmbeddedJpeg> {
59 let mut p = start + 2;
60 let mut size: Option<(u16, u16)> = None;
61 let mut has_exif = false;
62
63 loop {
64 if *data.get(p)? != 0xFF {
65 return None;
66 }
67 while *data.get(p + 1)? == 0xFF {
69 p += 1;
70 }
71 let marker = *data.get(p + 1)?;
72 match marker {
73 0x01 | 0xD0..=0xD7 => {
75 p += 2;
76 continue;
77 }
78 0xD8 | 0xD9 => return None,
80 0xC0..=0xFE => {}
81 _ => return None,
82 }
83
84 let len = u16_be(data, p + 2)? as usize;
85 if len < 2 {
86 return None;
87 }
88 let segment = data.get(p + 4..p + 2 + len)?;
89
90 match marker {
91 0xC0..=0xC2 => {
93 if segment.len() < 6 || segment[0] != 8 {
94 return None;
95 }
96 let height = u16::from_be_bytes([segment[1], segment[2]]);
97 let width = u16::from_be_bytes([segment[3], segment[4]]);
98 if width == 0 || height == 0 {
99 return None;
100 }
101 size = Some((width, height));
102 }
103 0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF => return None,
106 0xE1 if segment.starts_with(b"Exif\0\0") => has_exif = true,
107 _ => {}
108 }
109
110 p += 2 + len;
111
112 if marker == 0xDA {
113 size?;
115 loop {
118 let offset = data.get(p..)?.iter().position(|&b| b == 0xFF)?;
119 p += offset;
120 match *data.get(p + 1)? {
121 0x00 | 0xD0..=0xD7 => p += 2,
122 0xFF => p += 1,
123 0xD9 => {
124 let (width, height) = size?;
125 return Some(EmbeddedJpeg {
126 start,
127 end: p + 2,
128 width,
129 height,
130 has_exif,
131 });
132 }
133 _ => break,
136 }
137 }
138 }
139 }
140}
141
142#[cfg(test)]
143pub(crate) mod tests {
144 use super::*;
145
146 pub fn fake_jpeg(sof: u8, width: u16, height: u16, exif: Option<&[u8]>) -> Vec<u8> {
151 let mut j = vec![0xFF, 0xD8];
152 if let Some(exif) = exif {
153 j.extend([0xFF, 0xE1]);
154 j.extend(((exif.len() + 2) as u16).to_be_bytes());
155 j.extend(exif);
156 }
157 j.extend([0xFF, 0xDB, 0x00, 0x43, 0x00]);
159 j.extend([1u8; 64]);
160 j.extend([0xFF, sof, 0x00, 0x0B, 0x08]);
162 j.extend(height.to_be_bytes());
163 j.extend(width.to_be_bytes());
164 j.extend([0x01, 0x01, 0x11, 0x00]);
165 j.extend([0xFF, 0xC4, 0x00, 0x03, 0x00]);
167 j.extend([0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00]);
169 j.extend([0x12, 0xFF, 0x00, 0x34, 0xFF, 0xD0, 0x56]);
170 j.extend([0xFF, 0xD9]);
171 j
172 }
173
174 #[test]
175 fn finds_streams_and_skips_noise() {
176 let big = fake_jpeg(0xC0, 6000, 4000, None);
177 let small = fake_jpeg(0xC2, 160, 120, None);
178 let mut data = b"junk \xFF\xD8\xFF not a jpeg".to_vec();
179 let big_start = data.len();
180 data.extend(&big);
181 data.extend(b"more junk");
182 let small_start = data.len();
183 data.extend(&small);
184 data.extend(b"tail");
185
186 let found = find_jpegs(&data);
187 assert_eq!(found.len(), 2);
188 assert_eq!(found[0].start, big_start);
189 assert_eq!(found[0].end, big_start + big.len());
190 assert_eq!((found[0].width, found[0].height), (6000, 4000));
191 assert_eq!(found[1].start, small_start);
192 assert_eq!((found[1].width, found[1].height), (160, 120));
193 }
194
195 #[test]
196 fn rejects_lossless_sensor_data() {
197 let data = fake_jpeg(0xC3, 6000, 4000, None);
198 assert!(find_jpegs(&data).is_empty());
199 }
200
201 #[test]
202 fn nested_thumbnail_is_not_listed() {
203 let thumb = fake_jpeg(0xC0, 160, 120, None);
204 let mut exif = b"Exif\0\0".to_vec();
205 exif.extend(&thumb);
206 let data = fake_jpeg(0xC0, 1920, 1280, Some(&exif));
207
208 let found = find_jpegs(&data);
209 assert_eq!(found.len(), 1);
210 assert_eq!(found[0].width, 1920);
211 assert!(found[0].has_exif);
212 }
213
214 #[test]
215 fn truncated_stream_is_ignored() {
216 let mut data = fake_jpeg(0xC0, 1920, 1280, None);
217 data.truncate(data.len() - 2);
218 assert!(find_jpegs(&data).is_empty());
219 }
220}