1pub(crate) const MIN_SIDE: u32 = 280;
5pub(crate) const MAX_SIDE: u32 = 1568;
6const MAX_BYTES: usize = 16 * 1024 * 1024;
7
8pub const PNG_SIGNATURE: &[u8] = b"\x89PNG\r\n\x1a\n";
10
11const PNG_HEADER_LENGTH: u32 = 13;
13const PNG_HEADER_END: usize = 33;
14const PNG_CHUNK_START: usize = 8;
15const PNG_CRC: usize = 4;
16const JPEG_START: &[u8] = &[0xFF, 0xD8];
17const EXIF_START: &[u8] = b"Exif\0\0";
18const ORIENTATION_TAG: u16 = 0x0112;
19const UPRIGHT: u16 = 1;
20const TIFF_ENTRY: usize = 12;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct Header {
25 pub width: u32,
27 pub height: u32,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum HeaderError {
35 Format,
37 Header,
39 Size {
41 width: u32,
43 height: u32,
45 },
46 Bytes {
48 size: usize,
50 },
51 Orientation {
53 orientation: u16,
55 },
56}
57
58#[cfg(not(feature = "image"))]
60macro_rules! fix {
61 ($action:literal) => {
62 concat!("turn on the `image` feature of system2 to ", $action)
63 };
64}
65
66#[cfg(feature = "image")]
67macro_rules! fix {
68 ($action:literal) => {
69 concat!(
70 "pass a path to `image!`, or call `Image::new`, to ",
71 $action
72 )
73 };
74}
75
76impl HeaderError {
77 #[must_use]
80 pub const fn message(self) -> &'static str {
81 match self {
82 HeaderError::Format => concat!("the image must be PNG or JPEG: ", fix!("convert it")),
83 HeaderError::Header => "the image must have a complete PNG or JPEG header",
84 HeaderError::Size { .. } => concat!(
85 "each side of the image must be from 280 to 1568 px: ",
86 fix!("scale it")
87 ),
88 HeaderError::Bytes { .. } => concat!(
89 "the image must have at most 16 MiB: ",
90 fix!("make it smaller")
91 ),
92 HeaderError::Orientation { .. } => concat!(
93 "the image must be upright, but its EXIF orientation turns or mirrors it, and the \
94 API ignores the orientation: ",
95 fix!("turn its pixels")
96 ),
97 }
98 }
99}
100
101pub const fn read_header(bytes: &[u8]) -> Result<Header, HeaderError> {
109 if bytes.len() > MAX_BYTES {
110 let size = bytes.len();
111 return Err(HeaderError::Bytes { size });
112 }
113 let read = if same_at(bytes, 0, PNG_SIGNATURE) {
114 read_png(bytes)
115 } else if same_at(bytes, 0, JPEG_START) {
116 read_jpeg(bytes)
117 } else {
118 return Err(HeaderError::Format);
119 };
120 let (header, orientation) = match read {
121 Ok(read) => read,
122 Err(error) => return Err(error),
123 };
124 let Header { width, height } = header;
125 if !fits(width) || !fits(height) {
126 return Err(HeaderError::Size { width, height });
127 }
128
129 match orientation {
131 2..=8 => Err(HeaderError::Orientation { orientation }),
132 _ => Ok(header),
133 }
134}
135
136pub(crate) const fn fits(side: u32) -> bool {
137 side >= MIN_SIDE && side <= MAX_SIDE
138}
139
140type Read = Result<(Header, u16), HeaderError>;
142
143const fn read_png(bytes: &[u8]) -> Read {
145 if bytes.len() < PNG_HEADER_END
146 || u32_at(bytes, 8, true) != PNG_HEADER_LENGTH
147 || !same_at(bytes, 12, b"IHDR")
148 {
149 return Err(HeaderError::Header);
150 }
151 let width = u32_at(bytes, 16, true);
152 let height = u32_at(bytes, 20, true);
153 let mut orientation = UPRIGHT;
154 let mut chunk = PNG_SIGNATURE.len();
155 while chunk + PNG_CHUNK_START <= bytes.len() {
156 let data = chunk + PNG_CHUNK_START;
157 let end = data.saturating_add(u32_at(bytes, chunk, true) as usize);
158 if same_at(bytes, chunk + 4, b"IDAT") || end > bytes.len() {
159 break;
160 }
161 if same_at(bytes, chunk + 4, b"eXIf") {
162 orientation = exif_orientation(bytes, data, end);
163 }
164 chunk = end + PNG_CRC;
165 }
166 Ok((Header { width, height }, orientation))
167}
168
169const fn read_jpeg(bytes: &[u8]) -> Read {
171 let mut orientation = UPRIGHT;
172 let mut segment = JPEG_START.len();
173 while segment + 4 <= bytes.len() {
174 if bytes[segment] != 0xFF {
175 return Err(HeaderError::Header);
176 }
177 let marker = bytes[segment + 1];
178 let data = segment + 4;
179 let end = segment + 2 + u16_at(bytes, segment + 2, true) as usize;
180 if end > bytes.len() {
181 return Err(HeaderError::Header);
182 }
183 if is_frame(marker) {
184 if end < data + 5 {
185 return Err(HeaderError::Header);
186 }
187 let height = u16_at(bytes, data + 1, true) as u32;
188 let width = u16_at(bytes, data + 3, true) as u32;
189 return Ok((Header { width, height }, orientation));
190 }
191 if marker == 0xE1 && same_at(bytes, data, EXIF_START) {
192 orientation = exif_orientation(bytes, data + EXIF_START.len(), end);
193 }
194 segment = end;
195 }
196 Err(HeaderError::Header)
197}
198
199const fn is_frame(marker: u8) -> bool {
201 matches!(marker, 0xC0..=0xCF) && !matches!(marker, 0xC4 | 0xC8 | 0xCC)
202}
203
204const fn exif_orientation(bytes: &[u8], start: usize, end: usize) -> u16 {
207 if start + 8 > end {
208 return UPRIGHT;
209 }
210 let big_endian = if same_at(bytes, start, b"MM") {
211 true
212 } else if same_at(bytes, start, b"II") {
213 false
214 } else {
215 return UPRIGHT;
216 };
217 let directory = start.saturating_add(u32_at(bytes, start + 4, big_endian) as usize);
218 if directory.saturating_add(2) > end {
219 return UPRIGHT;
220 }
221 let entry_count = u16_at(bytes, directory, big_endian) as usize;
222 let mut entry_index = 0;
223 while entry_index < entry_count {
224 let entry = directory + 2 + entry_index * TIFF_ENTRY;
225 if entry + TIFF_ENTRY > end {
226 return UPRIGHT;
227 }
228 if u16_at(bytes, entry, big_endian) == ORIENTATION_TAG {
229 return u16_at(bytes, entry + 8, big_endian);
230 }
231 entry_index += 1;
232 }
233 UPRIGHT
234}
235
236const fn same_at(bytes: &[u8], at: usize, expected: &[u8]) -> bool {
238 if at + expected.len() > bytes.len() {
239 return false;
240 }
241 let mut index = 0;
242 while index < expected.len() {
243 if bytes[at + index] != expected[index] {
244 return false;
245 }
246 index += 1;
247 }
248 true
249}
250
251const fn u16_at(bytes: &[u8], at: usize, big_endian: bool) -> u16 {
252 let pair = [bytes[at], bytes[at + 1]];
253 if big_endian {
254 u16::from_be_bytes(pair)
255 } else {
256 u16::from_le_bytes(pair)
257 }
258}
259
260const fn u32_at(bytes: &[u8], at: usize, big_endian: bool) -> u32 {
261 let quad = [bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]];
262 if big_endian {
263 u32::from_be_bytes(quad)
264 } else {
265 u32::from_le_bytes(quad)
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::{Header, HeaderError, read_header};
272
273 fn png(width: u32, height: u32) -> Vec<u8> {
274 let mut header = [width.to_be_bytes(), height.to_be_bytes()].concat();
275 header.extend([8, 2, 0, 0, 0]);
276 [&b"\x89PNG\r\n\x1a\n"[..], &png_chunk(b"IHDR", &header)].concat()
277 }
278
279 fn png_chunk(chunk_type: &[u8], data: &[u8]) -> Vec<u8> {
280 let length = u32::try_from(data.len()).unwrap().to_be_bytes();
281 [&length[..], chunk_type, data, &[0; 4]].concat()
282 }
283
284 fn jpeg_segment(marker: u8, data: &[u8]) -> Vec<u8> {
285 let length = u16::try_from(data.len() + 2).unwrap().to_be_bytes();
286 [&[0xFF, marker][..], &length, data].concat()
287 }
288
289 fn jpeg_with(width: u16, height: u16, marker: u8, app1: Option<&[u8]>) -> Vec<u8> {
291 let mut frame = vec![8];
292 frame.extend(height.to_be_bytes());
293 frame.extend(width.to_be_bytes());
294 frame.extend([1, 1, 0x11, 0]);
295 let app1 = app1
296 .map(|data| jpeg_segment(0xE1, data))
297 .unwrap_or_default();
298 [
299 &[0xFF, 0xD8][..],
300 &app1,
301 &jpeg_segment(marker, &frame),
302 &[0xFF, 0xD9],
303 ]
304 .concat()
305 }
306
307 fn jpeg(width: u16, height: u16) -> Vec<u8> {
308 jpeg_with(width, height, 0xC0, None)
309 }
310
311 fn tiff(orientation: u16, big_endian: bool) -> Vec<u8> {
313 let u16_bytes = |value: u16| {
314 if big_endian {
315 value.to_be_bytes()
316 } else {
317 value.to_le_bytes()
318 }
319 };
320 let u32_bytes = |value: u32| {
321 if big_endian {
322 value.to_be_bytes()
323 } else {
324 value.to_le_bytes()
325 }
326 };
327 let order: &[u8] = if big_endian { b"MM" } else { b"II" };
328 [
329 order,
330 &u16_bytes(42),
331 &u32_bytes(8),
332 &u16_bytes(1),
333 &u16_bytes(0x0112),
334 &u16_bytes(3),
335 &u32_bytes(1),
336 &u16_bytes(orientation),
337 &[0, 0],
338 ]
339 .concat()
340 }
341
342 fn exif(tiff: &[u8]) -> Vec<u8> {
343 [&b"Exif\0\0"[..], tiff].concat()
344 }
345
346 fn header(width: u32, height: u32) -> Header {
347 Header { width, height }
348 }
349
350 #[test]
351 fn read_header_reads_the_size_of_a_png_and_a_jpeg() {
352 let progressive = jpeg_with(500, 700, 0xC2, None);
353 let after_xmp = jpeg_with(
354 640,
355 480,
356 0xC0,
357 Some(b"http://ns.adobe.com/xap/1.0/\0<x:xmpmeta/>"),
358 );
359
360 let after_table = [
362 &[0xFF, 0xD8][..],
363 &jpeg_segment(0xC4, &[0; 20]),
364 &jpeg(640, 480)[2..],
365 ]
366 .concat();
367 let cases = [
368 (png(400, 300), Ok(header(400, 300))),
369 (jpeg(640, 480), Ok(header(640, 480))),
370 (progressive, Ok(header(500, 700))),
371 (after_xmp, Ok(header(640, 480))),
372 (after_table, Ok(header(640, 480))),
373 ];
374 for (bytes, header) in cases {
375 assert_eq!(read_header(&bytes), header, "{bytes:?}");
376 }
377 }
378
379 #[test]
380 fn read_header_takes_each_side_from_280_to_1568_px() {
381 let cases = [
382 (280, 280, true),
383 (1568, 1568, true),
384 (279, 500, false),
385 (500, 279, false),
386 (1569, 500, false),
387 (500, 1569, false),
388 ];
389 for (width, height, valid) in cases {
390 let expected = if valid {
391 Ok(header(width.into(), height.into()))
392 } else {
393 Err(HeaderError::Size {
394 width: width.into(),
395 height: height.into(),
396 })
397 };
398
399 assert_eq!(read_header(&png(width.into(), height.into())), expected);
400 assert_eq!(read_header(&jpeg(width, height)), expected);
401 }
402 }
403
404 #[test]
405 fn read_header_takes_at_most_16_mib() {
406 let mut bytes = png(300, 300);
407 bytes.resize(16 * 1024 * 1024, 0);
408 assert_eq!(read_header(&bytes), Ok(header(300, 300)));
409
410 bytes.push(0);
411 assert_eq!(
412 read_header(&bytes),
413 Err(HeaderError::Bytes { size: 16_777_217 })
414 );
415 }
416
417 #[test]
418 fn read_header_refuses_other_formats() {
419 let others: [&[u8]; 4] = [
420 b"",
421 b"GIF89a\x01\x00\x01\x00",
422 b"RIFF\0\0\0\0WEBPVP8 ",
423 b"\xFF",
424 ];
425 for bytes in others {
426 assert_eq!(read_header(bytes), Err(HeaderError::Format), "{bytes:?}");
427 }
428 }
429
430 #[test]
431 fn read_header_refuses_a_broken_header() {
432 let signature = b"\x89PNG\r\n\x1a\n".to_vec();
433 let short_png = png(300, 300)[..20].to_vec();
434 let no_crc = png(300, 300)[..32].to_vec();
435 let mut long_header = png(300, 300);
436 long_header[11] = 14;
437 let data_first = [&signature[..], &png_chunk(b"IDAT", &[0; 13])].concat();
438 let no_frame = vec![0xFF, 0xD8, 0xFF, 0xD9];
439 let past_the_end = vec![0xFF, 0xD8, 0xFF, 0xE0, 0x10, 0x00, 0x00];
440 let short_frame = [
441 &[0xFF, 0xD8][..],
442 &jpeg_segment(0xC0, &[8, 1, 0, 2]),
443 &[0xFF, 0xD9],
444 ]
445 .concat();
446 let not_a_marker = vec![0xFF, 0xD8, 0x00, 0xC0, 0x00, 0x11];
447 let cases = [
448 signature,
449 short_png,
450 no_crc,
451 long_header,
452 data_first,
453 no_frame,
454 past_the_end,
455 short_frame,
456 not_a_marker,
457 ];
458 for bytes in cases {
459 assert_eq!(read_header(&bytes), Err(HeaderError::Header), "{bytes:?}");
460 }
461 }
462
463 #[test]
464 fn read_header_refuses_an_image_that_its_exif_turns() {
465 let png_exif = [png(400, 300), png_chunk(b"eXIf", &tiff(3, true))].concat();
466 let cases = [
467 (jpeg_with(640, 480, 0xC0, Some(&exif(&tiff(6, false)))), 6),
468 (jpeg_with(640, 480, 0xC0, Some(&exif(&tiff(8, true)))), 8),
469 (jpeg_with(640, 480, 0xC0, Some(&exif(&tiff(2, false)))), 2),
470 (png_exif, 3),
471 ];
472 for (bytes, orientation) in cases {
473 assert_eq!(
474 read_header(&bytes),
475 Err(HeaderError::Orientation { orientation })
476 );
477 }
478 }
479
480 #[test]
481 fn read_header_takes_an_upright_image() {
482 let upright = jpeg_with(640, 480, 0xC0, Some(&exif(&tiff(1, false))));
483 let unknown = jpeg_with(640, 480, 0xC0, Some(&exif(&tiff(9, false))));
484 let png_upright = [png(400, 300), png_chunk(b"eXIf", &tiff(1, true))].concat();
485
486 let after_data = [
488 png(400, 300),
489 png_chunk(b"IDAT", &[0; 4]),
490 png_chunk(b"eXIf", &tiff(6, true)),
491 ]
492 .concat();
493 let chunk_past_the_end = [&png(400, 300)[..], &100u32.to_be_bytes(), b"eXIf"].concat();
494 for bytes in [
495 upright,
496 unknown,
497 png_upright,
498 after_data,
499 chunk_past_the_end,
500 ] {
501 assert!(read_header(&bytes).is_ok(), "{bytes:?}");
502 }
503 }
504
505 #[test]
506 fn read_header_ignores_exif_that_it_cannot_read() {
507 let mut other_tag = tiff(6, false);
508 other_tag[10..12].copy_from_slice(&0x0110u16.to_le_bytes());
509 let other_order = [&b"XX"[..], &tiff(6, false)[2..]].concat();
510 let short_entry = tiff(6, false)[..21].to_vec();
511 let far_directory = [&b"II\x2a\x00"[..], &500u32.to_le_bytes()].concat();
512 let cases = [
513 other_tag,
514 other_order,
515 short_entry,
516 far_directory,
517 Vec::new(),
518 ];
519 for tiff in cases {
520 let bytes = jpeg_with(640, 480, 0xC0, Some(&exif(&tiff)));
521
522 assert_eq!(read_header(&bytes), Ok(header(640, 480)), "{tiff:?}");
523 }
524 }
525
526 #[test]
527 fn message_tells_the_rule() {
528 let errors = [
529 HeaderError::Format,
530 HeaderError::Header,
531 HeaderError::Size {
532 width: 1,
533 height: 1,
534 },
535 HeaderError::Bytes { size: 1 },
536 HeaderError::Orientation { orientation: 6 },
537 ];
538 #[cfg(not(feature = "image"))]
539 let fix = "turn on the `image` feature of system2 to";
540 #[cfg(feature = "image")]
541 let fix = "pass a path to `image!`, or call `Image::new`, to";
542 let messages = [
543 format!("the image must be PNG or JPEG: {fix} convert it"),
544 "the image must have a complete PNG or JPEG header".to_owned(),
545 format!("each side of the image must be from 280 to 1568 px: {fix} scale it"),
546 format!("the image must have at most 16 MiB: {fix} make it smaller"),
547 format!(
548 "the image must be upright, but its EXIF orientation turns or mirrors it, and the \
549 API ignores the orientation: {fix} turn its pixels"
550 ),
551 ];
552 assert_eq!(errors.map(HeaderError::message), messages);
553 }
554}