diff --git a/src/jpeg.imageio/jpeginput.cpp b/src/jpeg.imageio/jpeginput.cpp index fc78f8eeca..e606ecd872 100644 --- a/src/jpeg.imageio/jpeginput.cpp +++ b/src/jpeg.imageio/jpeginput.cpp @@ -52,6 +52,28 @@ OIIO_PLUGIN_EXPORTS_END static const uint8_t JPEG_MAGIC1 = 0xff; static const uint8_t JPEG_MAGIC2 = 0xd8; +static const char exif_marker_prefix[] = "Exif\0"; + +static const char icc_marker_prefix[] = "ICC_PROFILE"; + +static bool +is_exif_marker(jpeg_saved_marker_ptr marker) +{ + return marker->marker == (JPEG_APP0 + 1) + && marker->data_length >= sizeof(exif_marker_prefix) + && !memcmp(marker->data, exif_marker_prefix, + sizeof(exif_marker_prefix)); +} + +static bool +is_icc_profile_marker(jpeg_saved_marker_ptr marker) +{ + return marker->marker == (JPEG_APP0 + 2) + && marker->data_length >= ICC_HEADER_SIZE + && !memcmp(marker->data, icc_marker_prefix, + sizeof(icc_marker_prefix)); +} + // For explanations of the error handling, see the "example.c" in the // libjpeg distribution. @@ -271,8 +293,7 @@ JpgInput::open(const std::string& name, ImageSpec& newspec) m_spec.attribute(JPEG_SUBSAMPLING_ATTR, subsampling); for (jpeg_saved_marker_ptr m = m_cinfo.marker_list; m; m = m->next) { - if (m->marker == (JPEG_APP0 + 1) && m->data_length >= 4 - && !strncmp((const char*)m->data, "Exif", 4)) { + if (is_exif_marker(m)) { // The block starts with "Exif\0\0", so skip 6 bytes to get // to the start of the actual Exif data TIFF directory decode_exif(string_view((char*)m->data + 6, m->data_length - 6), @@ -394,8 +415,7 @@ JpgInput::read_icc_profile(j_decompress_ptr cinfo, ImageSpec& spec) memset(marker_present, 0, (MAX_SEQ_NO + 1)); for (jpeg_saved_marker_ptr m = cinfo->marker_list; m; m = m->next) { - if (m->marker == (JPEG_APP0 + 2) - && !strcmp((const char*)m->data, "ICC_PROFILE")) { + if (is_icc_profile_marker(m)) { if (num_markers == 0) num_markers = GETJOCTET(m->data[13]); else if (num_markers != GETJOCTET(m->data[13])) @@ -427,8 +447,7 @@ JpgInput::read_icc_profile(j_decompress_ptr cinfo, ImageSpec& spec) // and fill it in for (jpeg_saved_marker_ptr m = cinfo->marker_list; m; m = m->next) { - if (m->marker == (JPEG_APP0 + 2) - && !strcmp((const char*)m->data, "ICC_PROFILE")) { + if (is_icc_profile_marker(m)) { int seq_no = GETJOCTET(m->data[12]); if (data_offset[seq_no] + data_length[seq_no] > icc_buf.size()) { errorfmt("Possible corrupt file, invalid ICC profile\n"); diff --git a/testsuite/jpeg-corrupt/ref/out-alt2.txt b/testsuite/jpeg-corrupt/ref/out-alt2.txt index c7a7219879..4bb15103d5 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt2.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt2.txt @@ -44,6 +44,11 @@ src/corrupt-icc-4552.jpg : 1500 x 1000, 3 channel, uint8 jpeg ICCProfile:rendering_intent: "Unknown" jpeg:subsampling: "4:2:0" oiio:ColorSpace: "srgb_rec709_scene" +short-exif-app1-len4-ok +short-exif-app1-len5-ok +short-icc-app2-len11-ok +short-icc-app2-len12-ok +short-icc-app2-len13-ok oiiotool ERROR: read : JPEG error: Corrupt JPEG data: 256 extraneous bytes before marker 0xdb ("src/corrupt-iptc-8011.jpg") Corrupted IPTC data Full command line was: diff --git a/testsuite/jpeg-corrupt/ref/out-alt3.txt b/testsuite/jpeg-corrupt/ref/out-alt3.txt index 1ffe664407..459988f3cf 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt3.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt3.txt @@ -43,6 +43,11 @@ src/corrupt-icc-4552.jpg : 1500 x 1000, 3 channel, uint8 jpeg ICCProfile:rendering_intent: "Unknown" jpeg:subsampling: "4:2:0" oiio:ColorSpace: "srgb_rec709_scene" +short-exif-app1-len4-ok +short-exif-app1-len5-ok +short-icc-app2-len11-ok +short-icc-app2-len12-ok +short-icc-app2-len13-ok oiiotool ERROR: read : JPEG error: Corrupt JPEG data: 256 extraneous bytes before marker 0xdb ("src/corrupt-iptc-8011.jpg") Corrupted IPTC data Full command line was: diff --git a/testsuite/jpeg-corrupt/ref/out-alt4.txt b/testsuite/jpeg-corrupt/ref/out-alt4.txt index 38501c30a6..22a7d09d32 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt4.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt4.txt @@ -44,6 +44,11 @@ src/corrupt-icc-4552.jpg : 1500 x 1000, 3 channel, uint8 jpeg ICCProfile:rendering_intent: "Unknown" jpeg:subsampling: "4:2:0" oiio:ColorSpace: "srgb_rec709_scene" +short-exif-app1-len4-ok +short-exif-app1-len5-ok +short-icc-app2-len11-ok +short-icc-app2-len12-ok +short-icc-app2-len13-ok oiiotool ERROR: read : JPEG error: Corrupt JPEG data: 256 extraneous bytes before marker 0xdb ("src/corrupt-iptc-8011.jpg") Corrupted IPTC data Full command line was: diff --git a/testsuite/jpeg-corrupt/ref/out-alt5.txt b/testsuite/jpeg-corrupt/ref/out-alt5.txt index 54bf6854f2..d8aa9d2c5f 100644 --- a/testsuite/jpeg-corrupt/ref/out-alt5.txt +++ b/testsuite/jpeg-corrupt/ref/out-alt5.txt @@ -46,6 +46,11 @@ src/corrupt-icc-4552.jpg : 1500 x 1000, 3 channel, uint8 jpeg ICCProfile:rendering_intent: "Unknown" jpeg:subsampling: "4:2:0" oiio:ColorSpace: "srgb_rec709_scene" +short-exif-app1-len4-ok +short-exif-app1-len5-ok +short-icc-app2-len11-ok +short-icc-app2-len12-ok +short-icc-app2-len13-ok oiiotool ERROR: read : JPEG error: Corrupt JPEG data: 256 extraneous bytes before marker 0xdb ("src/corrupt-iptc-8011.jpg") Corrupted IPTC data Full command line was: diff --git a/testsuite/jpeg-corrupt/ref/out.txt b/testsuite/jpeg-corrupt/ref/out.txt index 4c13641934..86e3266344 100644 --- a/testsuite/jpeg-corrupt/ref/out.txt +++ b/testsuite/jpeg-corrupt/ref/out.txt @@ -44,6 +44,11 @@ src/corrupt-icc-4552.jpg : 1500 x 1000, 3 channel, uint8 jpeg ICCProfile:rendering_intent: "Unknown" jpeg:subsampling: "4:2:0" oiio:ColorSpace: "srgb_rec709_scene" +short-exif-app1-len4-ok +short-exif-app1-len5-ok +short-icc-app2-len11-ok +short-icc-app2-len12-ok +short-icc-app2-len13-ok oiiotool ERROR: read : JPEG error: Corrupt JPEG data: 256 extraneous bytes before marker 0xdb ("src/corrupt-iptc-8011.jpg") Corrupted IPTC data Full command line was: diff --git a/testsuite/jpeg-corrupt/run.py b/testsuite/jpeg-corrupt/run.py index 3d9f89e545..84f24a22d9 100755 --- a/testsuite/jpeg-corrupt/run.py +++ b/testsuite/jpeg-corrupt/run.py @@ -8,6 +8,10 @@ failureok = 1 redirect = ' >> out.txt 2>&1 ' +command += oiiotool("--create 1x1 3 -d uint8 -o base-short-marker.jpg") +command += run_app(pythonbin + " src/make-short-marker-jpegs.py", silent=True) + + # This file has a corrupted Exif block in the metadata. It used to # crash on some platforms, on others would be caught by address sanitizer. # Fixed by #1635. This test serves to guard against regressions. @@ -25,5 +29,19 @@ # This file has a corrupted ICC profile block command += info_command ("src/corrupt-icc-4552.jpg", safematch=True) +# These files have short APP1/APP2 metadata marker payloads that used to be +# read past their saved-marker buffers before being ignored. Use iconvert to +# a null output to force a full input read. +command += iconvert("short-exif-app1-len4.jpg out.null", + successmessage="short-exif-app1-len4-ok") +command += iconvert("short-exif-app1-len5.jpg out.null", + successmessage="short-exif-app1-len5-ok") +command += iconvert("short-icc-app2-len11.jpg out.null", + successmessage="short-icc-app2-len11-ok") +command += iconvert("short-icc-app2-len12.jpg out.null", + successmessage="short-icc-app2-len12-ok") +command += iconvert("short-icc-app2-len13.jpg out.null", + successmessage="short-icc-app2-len13-ok") + # This file had corrupted IPTC data command += oiiotool("-oiioattrib imageinput:strict 1 -info -v src/corrupt-iptc-8011.jpg") diff --git a/testsuite/jpeg-corrupt/src/make-short-marker-jpegs.py b/testsuite/jpeg-corrupt/src/make-short-marker-jpegs.py new file mode 100644 index 0000000000..562709722b --- /dev/null +++ b/testsuite/jpeg-corrupt/src/make-short-marker-jpegs.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +from pathlib import Path + + +APP1 = 0xE1 +APP2 = 0xE2 +BASE = Path("base-short-marker.jpg") + + +def marker(marker_id, payload): + length = len(payload) + 2 + return b"\xff" + bytes([marker_id]) + length.to_bytes(2, "big") + payload + + +def write_with_marker(name, marker_id, payload): + data = BASE.read_bytes() + if data[:2] != b"\xff\xd8": + raise RuntimeError(f"{BASE} is not a JPEG stream") + Path(name).write_bytes(data[:2] + marker(marker_id, payload) + data[2:]) + + +write_with_marker("short-exif-app1-len4.jpg", APP1, b"Exif") +write_with_marker("short-exif-app1-len5.jpg", APP1, b"Exif\0") +write_with_marker("short-icc-app2-len11.jpg", APP2, b"ICC_PROFILE") +write_with_marker("short-icc-app2-len12.jpg", APP2, b"ICC_PROFILE\0") +write_with_marker("short-icc-app2-len13.jpg", APP2, b"ICC_PROFILE\0\1") diff --git a/testsuite/runtest.py b/testsuite/runtest.py index 052b68434a..e37e1cca7a 100755 --- a/testsuite/runtest.py +++ b/testsuite/runtest.py @@ -325,8 +325,11 @@ def testtex_command (file: str, extraargs: str="", silent: bool=False, concat: b # Construct a command that will run iconvert and append its output to out.txt -def iconvert (args: str, silent: bool=False, concat: bool=True, failureok: bool=False) -> str: +def iconvert (args: str, silent: bool=False, concat: bool=True, + failureok: bool=False, successmessage: str="") -> str: cmd = (oiio_app("iconvert") + " " + args) + if successmessage: + cmd = "(" + cmd + " && echo " + successmessage + ")" if not silent : cmd += redirect if failureok :