Commit fb8d9457 for libheif
commit fb8d945732b0c58600cfb1101cfe595c74a6322b
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Sat Sep 5 21:12:04 2026 +0200
Fix coded/visible rectangle mismatch in the WebCodecs decoder plugin (GHSA-rhgw-q5g8-xjh2)
The WebCodecs decoder plugin sized its pixel buffer with
VideoFrame.allocationSize() and filled it with VideoFrame.copyTo(). Both default
to the frame's visible rectangle. It then reported VideoFrame.codedWidth and
codedHeight to the C++ side, which used them as the row length and row count of
every copy loop. The gap between the two rectangles is the HEVC conformance
window, which the file being decoded sets in its SPS, so a crafted HEIC with a
large coded picture and a tiny visible window made the plugin read far past the
end of the buffer and return the bytes as image pixels. Declaring 'ispe' at the
coded size satisfies both the pre-decode SPS size limit and the post-decode
exact-size check. The NV12 and planar branches over-read; the RGBA branch was
safe only because its buffer happened to be sized for the coded rectangle.
Only emscripten builds with WITH_WEBCODECS=ON (off by default, experimental)
that do not also link libde265 are affected. The over-read stays within the
wasm module's linear memory and there is no out-of-bounds write.
JavaScript: read decoded.visibleRect once, pass it explicitly as 'rect' to both
allocationSize() and copyTo(), and report that rectangle's size as 'width' and
'height'. The keys are renamed so that nothing can pick up a coded value by
accident. The canvas fallback for frames without a copyable format draws and
reads the visible rectangle too. Returning the coded size was also a plain
correctness bug: libheif's 'ispe' holds the conformance-window-cropped size
(odd sizes get a 'clap'), so the decoded image did not match 'ispe' for any
image whose coded size differs from its visible size.
C++: check every plane layout reported by the browser against the buffer
length (offset + (rows-1)*stride + row_bytes <= len) before copying, bound the
reported width and height so that per-row byte counts cannot overflow, and
reject zero sizes. Pass the security limits through the plugin and allocate
with heif_image_add_plane_safe() like the other decoder plugins. Check the
coded SPS size against max_image_size_pixels before handing the bitstream to
the browser, since the plugin honours the last SPS in the NAL stream rather
than the one in 'hvcC'. Round chroma plane sizes up to match libheif's
convention.
chroma_format_idc is now range-checked in the SPS parser instead of being
stored into a uint8_t unchecked. This is done both in the plugin's copy of the
parser and in parse_sps_for_hvcC_configuration(), whose result is also cast to
heif_chroma in Decoder_HEVC.
Tests: the new hevc_sps test parses a real x265 SPS taken from
tests/data/rainbow-451x461.heic, checks the visible (452x462) and coded
(456x464) sizes, and checks that an SPS with chroma_format_idc=4 is rejected.
The plugin itself has no native build; its changes were type-checked against
stub emscripten headers with clang and GCC, and the embedded JavaScript was
parsed with node.
diff --git a/libheif/codecs/hevc_boxes.cc b/libheif/codecs/hevc_boxes.cc
index a185cb5a..2af42e7a 100644
--- a/libheif/codecs/hevc_boxes.cc
+++ b/libheif/codecs/hevc_boxes.cc
@@ -669,6 +669,13 @@ Error parse_sps_for_hvcC_configuration(const uint8_t* sps, size_t size,
!reader.get_uvlc(&value)) {
return invalidUVLC;
}
+ if (value > 3) {
+ // chroma_format_idc is in the range 0..3 (H.265 section 7.4.3.2.1). The
+ // value is later cast to heif_chroma, so it must not be left unchecked.
+ return Error{heif_error_Invalid_input,
+ heif_suberror_Invalid_parameter_value,
+ "SPS chroma_format_idc out of range"};
+ }
config->chroma_format = (uint8_t) value;
if (config->chroma_format == 3) {
diff --git a/libheif/plugins/decoder_webcodecs.cc b/libheif/plugins/decoder_webcodecs.cc
index c302f359..e939325c 100644
--- a/libheif/plugins/decoder_webcodecs.cc
+++ b/libheif/plugins/decoder_webcodecs.cc
@@ -30,6 +30,7 @@
#include <emscripten/emscripten.h>
#include <cstdio>
#include <emscripten/bind.h>
+#include <limits>
#include <memory>
#include <queue>
#include <string>
@@ -88,19 +89,24 @@ EM_JS(emscripten::EM_VAL, decode_with_browser_hevc, (const char *codec_ptr, uint
}
function handleEmptyFormat(decoded) {
- const canvas = new OffscreenCanvas(decoded.codedWidth, decoded.codedHeight);
+ // Use the visible rectangle, not the coded rectangle. The coded rectangle
+ // may include non-visible padding (HEVC conformance window) that is not
+ // part of the image.
+ const width = decoded.visibleRect.width;
+ const height = decoded.visibleRect.height;
+ const canvas = new OffscreenCanvas(width, height);
const context = canvas.getContext('2d');
- context.drawImage(decoded, 0, 0);
- const imageData = context.getImageData(0, 0, decoded.codedWidth, decoded.codedHeight);
+ context.drawImage(decoded, 0, 0, width, height);
+ const imageData = context.getImageData(0, 0, width, height);
const data = imageData.data;
const format = 'RGBA';
- const planes = [{offset: 0, stride: decoded.codedWidth * 4}];
+ const planes = [{offset: 0, stride: width * 4}];
callback(Emval.toHandle({
'buffer': data,
'format': format,
'planes': planes,
- 'codedWidth': decoded.codedWidth,
- 'codedHeight': decoded.codedHeight,
+ 'width': width,
+ 'height': height,
}));
decoded.close();
@@ -127,12 +133,23 @@ EM_JS(emscripten::EM_VAL, decode_with_browser_hevc, (const char *codec_ptr, uint
const nativeFormats = ['NV12', 'I420', 'I422', 'I444'];
const format = nativeFormats.includes(decoded.format) ? decoded.format : 'RGBA';
const fullRange = decoded.colorSpace ? decoded.colorSpace.fullRange : false;
+
+ // Always operate on the visible rectangle. allocationSize() and
+ // copyTo() default to it anyway, but pass it explicitly so that the
+ // buffer size, the plane layout and the dimensions reported to C++
+ // are guaranteed to describe the same rectangle. The coded rectangle
+ // (codedWidth x codedHeight) can be larger because of the HEVC
+ // conformance window, which is set by the file being decoded. It must
+ // never be used as the geometry of the copied buffer.
+ const rect = decoded.visibleRect;
+ const width = rect.width;
+ const height = rect.height;
const formatOptions = nativeFormats.includes(format) ?
- {} :
- {'format': format, 'colorSpace': 'srgb'};
+ {'rect': rect} :
+ {'rect': rect, 'format': format, 'colorSpace': 'srgb'};
const bufferSize = nativeFormats.includes(format) ?
- decoded.allocationSize() :
- decoded.codedWidth * decoded.codedHeight * 4;
+ decoded.allocationSize(formatOptions) :
+ width * height * 4;
const buffer = new Uint8Array(bufferSize);
@@ -143,8 +160,8 @@ EM_JS(emscripten::EM_VAL, decode_with_browser_hevc, (const char *codec_ptr, uint
'buffer': buffer,
'format': format,
'planes': planes,
- 'codedWidth': decoded.codedWidth,
- 'codedHeight': decoded.codedHeight,
+ 'width': width,
+ 'height': height,
'fullRange': fullRange,
}));
@@ -203,9 +220,13 @@ static std::vector<uint8_t> remove_start_code_emulation2(const uint8_t* sps, siz
}
+// Parses the SPS and fills 'config'. On return, 'width' and 'height' hold the
+// size of the visible image (after applying the conformance window), while
+// 'coded_width' and 'coded_height' hold the size of the coded picture.
Error parse_sps_for_hvcC_configuration2(const uint8_t* sps, size_t size,
HEVCDecoderConfigurationRecord* config,
- uint32_t* width, uint32_t* height)
+ uint32_t* width, uint32_t* height,
+ uint32_t* coded_width, uint32_t* coded_height)
{
// remove start-code emulation bytes from SPS header stream
@@ -280,6 +301,13 @@ Error parse_sps_for_hvcC_configuration2(const uint8_t* sps, size_t size,
!reader.get_uvlc(&value)) {
return invalidUVLC;
}
+ if (value > 3) {
+ // chroma_format_idc is in the range 0..3 (H.265 section 7.4.3.2.1). The
+ // value is later cast to heif_chroma, so it must not be left unchecked.
+ return Error{heif_error_Invalid_input,
+ heif_suberror_Invalid_parameter_value,
+ "SPS chroma_format_idc out of range"};
+ }
config->chroma_format = (uint8_t) value;
if (config->chroma_format == 3) {
@@ -291,6 +319,9 @@ Error parse_sps_for_hvcC_configuration2(const uint8_t* sps, size_t size,
return invalidUVLC;
}
+ *coded_width = *width;
+ *coded_height = *height;
+
bool conformance_window = reader.get_bits(1);
if (conformance_window) {
uint32_t left, right, top, bottom;
@@ -474,6 +505,59 @@ static void normalize_chroma_range(uint8_t* dst, int stride, int width, int heig
}
}
+// Returns the size of the chroma planes for the given chroma format. Odd luma
+// sizes are rounded up, matching the convention used throughout libheif.
+static void get_chroma_plane_size(heif_chroma chroma, int width, int height,
+ int* chroma_w, int* chroma_h)
+{
+ *chroma_w = width;
+ *chroma_h = height;
+ if (chroma == heif_chroma_420 || chroma == heif_chroma_monochrome) {
+ *chroma_w = (width + 1) / 2;
+ *chroma_h = (height + 1) / 2;
+ }
+ else if (chroma == heif_chroma_422) {
+ *chroma_w = (width + 1) / 2;
+ }
+}
+
+
+// Checks that a plane consisting of 'rows' rows of 'row_bytes' bytes each,
+// starting at 'offset' and with 'stride' bytes between consecutive rows, lies
+// entirely within a buffer of 'buffer_size' bytes.
+//
+// The plane layout is reported by the browser and the image geometry
+// ultimately comes from the file being decoded, so none of these values may be
+// trusted. Every copy loop that reads from the browser's buffer must be
+// preceded by this check.
+static bool plane_fits_in_buffer(size_t buffer_size,
+ int offset, int stride,
+ int rows, int row_bytes)
+{
+ if (offset < 0 || stride < 0 || rows <= 0 || row_bytes <= 0) {
+ return false;
+ }
+
+ if (stride < row_bytes) {
+ return false;
+ }
+
+ const uint64_t end = static_cast<uint64_t>(offset) +
+ static_cast<uint64_t>(rows - 1) * static_cast<uint64_t>(stride) +
+ static_cast<uint64_t>(row_bytes);
+ return end <= buffer_size;
+}
+
+
+static const heif_error kPlaneOutOfBoundsError = {
+ heif_error_Decoder_plugin_error,
+ heif_suberror_Unspecified,
+ "Decoding failed: plane layout reported by the browser exceeds the decoded buffer"};
+
+
+// The caller must have verified with plane_fits_in_buffer() that each source
+// plane covers 'height' rows of 'width' bytes (luma) and the corresponding
+// chroma plane size (see get_chroma_plane_size()).
static struct heif_error convert_planar_yuv_to_heif_image(
const uint8_t* y_src, int y_src_stride,
const uint8_t* u_src, int u_src_stride,
@@ -481,18 +565,13 @@ static struct heif_error convert_planar_yuv_to_heif_image(
int width, int height,
struct heif_image** out_img,
heif_chroma chroma,
- bool is_full_range) {
+ bool is_full_range,
+ const heif_security_limits* limits) {
heif_error err;
bool is_mono = chroma == heif_chroma_monochrome;
- int chroma_w = width;
- int chroma_h = height;
- if (chroma == heif_chroma_420 || is_mono) {
- chroma_w = width / 2;
- chroma_h = height / 2;
- } else if (chroma == heif_chroma_422) {
- chroma_w = width / 2;
- }
+ int chroma_w, chroma_h;
+ get_chroma_plane_size(chroma, width, height, &chroma_w, &chroma_h);
err = heif_image_create(
width, height,
@@ -504,8 +583,8 @@ static struct heif_error convert_planar_yuv_to_heif_image(
return err;
}
- err = heif_image_add_plane(
- *out_img, heif_channel_Y, width, height, 8);
+ err = heif_image_add_plane_safe(
+ *out_img, heif_channel_Y, width, height, 8, limits);
if (err.code) {
heif_image_release(*out_img);
return err;
@@ -525,17 +604,17 @@ static struct heif_error convert_planar_yuv_to_heif_image(
}
if (!is_mono) {
- err = heif_image_add_plane(
+ err = heif_image_add_plane_safe(
*out_img, heif_channel_Cb,
- chroma_w, chroma_h, 8);
+ chroma_w, chroma_h, 8, limits);
if (err.code) {
heif_image_release(*out_img);
return err;
}
- err = heif_image_add_plane(
+ err = heif_image_add_plane_safe(
*out_img, heif_channel_Cr,
- chroma_w, chroma_h, 8);
+ chroma_w, chroma_h, 8, limits);
if (err.code) {
heif_image_release(*out_img);
return err;
@@ -571,27 +650,36 @@ static struct heif_error convert_planar_yuv_to_heif_image(
}
static struct heif_error convert_nv12_to_heif_image(
- const std::unique_ptr<uint8_t[]>& buffer,
+ const std::unique_ptr<uint8_t[]>& buffer, size_t buffer_size,
int width, int height,
int y_offset, int y_src_stride,
int uv_offset, int uv_src_stride,
struct heif_image** out_img,
- heif_chroma chroma,
- bool is_full_range) {
- bool is_mono = chroma == heif_chroma_monochrome;
+ bool is_mono,
+ bool is_full_range,
+ const heif_security_limits* limits) {
+ if (!plane_fits_in_buffer(buffer_size, y_offset, y_src_stride, height, width)) {
+ return kPlaneOutOfBoundsError;
+ }
if (is_mono) {
return convert_planar_yuv_to_heif_image(
buffer.get() + y_offset, y_src_stride,
nullptr, 0, nullptr, 0,
width, height, out_img,
- heif_chroma_monochrome, is_full_range);
+ heif_chroma_monochrome, is_full_range, limits);
+ }
+
+ int chroma_w, chroma_h;
+ get_chroma_plane_size(heif_chroma_420, width, height, &chroma_w, &chroma_h);
+
+ // The interleaved UV plane has chroma_w Cb/Cr sample pairs per row.
+ if (!plane_fits_in_buffer(buffer_size, uv_offset, uv_src_stride, chroma_h, chroma_w * 2)) {
+ return kPlaneOutOfBoundsError;
}
- int chroma_w = width / 2;
- int chroma_h = height / 2;
- std::vector<uint8_t> u_buf(chroma_w * chroma_h);
- std::vector<uint8_t> v_buf(chroma_w * chroma_h);
+ std::vector<uint8_t> u_buf(static_cast<size_t>(chroma_w) * chroma_h);
+ std::vector<uint8_t> v_buf(static_cast<size_t>(chroma_w) * chroma_h);
for (int i = 0; i < chroma_h; ++i) {
const uint8_t* uv_row =
@@ -607,7 +695,7 @@ static struct heif_error convert_nv12_to_heif_image(
u_buf.data(), chroma_w,
v_buf.data(), chroma_w,
width, height, out_img,
- heif_chroma_420, is_full_range);
+ heif_chroma_420, is_full_range, limits);
}
/**
@@ -683,8 +771,9 @@ static void get_nal_units(struct webcodecs_decoder* decoder,
}
-static struct heif_error webcodecs_decode_image(void* decoder_raw,
- struct heif_image** out_img)
+static struct heif_error webcodecs_decode_image_with_limits(void* decoder_raw,
+ struct heif_image** out_img,
+ const heif_security_limits* limits)
{
struct webcodecs_decoder* decoder = (struct webcodecs_decoder*) decoder_raw;
*out_img = nullptr;
@@ -703,14 +792,35 @@ static struct heif_error webcodecs_decode_image(void* decoder_raw,
}
HEVCDecoderConfigurationRecord config;
- uint32_t w, h;
- Error err = parse_sps_for_hvcC_configuration2(sps_nal_unit.data.data(), sps_nal_unit.data.size(), &config, &w, &h);
+ uint32_t w, h; // visible size after applying the conformance window
+ uint32_t coded_w, coded_h; // size of the coded picture
+ Error err = parse_sps_for_hvcC_configuration2(sps_nal_unit.data.data(), sps_nal_unit.data.size(),
+ &config, &w, &h, &coded_w, &coded_h);
if (err != Error::Ok) {
return {heif_error_Decoder_plugin_error,
heif_suberror_Unspecified,
"Failed to parse SPS"};
}
+ // Reject coded picture sizes beyond the security limits before handing the
+ // bitstream to the browser. libheif checks the SPS stored in the hvcC box,
+ // but this plugin honours the last SPS in the NAL stream, which may differ.
+ if (coded_w == 0 || coded_h == 0 || w == 0 || h == 0) {
+ return {heif_error_Decoder_plugin_error,
+ heif_suberror_Invalid_image_size,
+ "SPS declares a zero-sized image"};
+ }
+
+ if (limits && limits->max_image_size_pixels > 0) {
+ const auto max_dim = static_cast<uint32_t>(std::numeric_limits<int>::max());
+ if (coded_w > max_dim || coded_h > max_dim ||
+ coded_w > limits->max_image_size_pixels / coded_h) {
+ return {heif_error_Memory_allocation_error,
+ heif_suberror_Security_limit_exceeded,
+ "SPS coded picture size exceeds the maximum image size"};
+ }
+ }
+
config.m_nal_array.push_back(HEVCDecoderConfigurationRecord::NalArray{0, HEVC_NAL_UNIT_VPS_NUT, {vps_nal_unit.data}});
config.m_nal_array.push_back(HEVCDecoderConfigurationRecord::NalArray{0, HEVC_NAL_UNIT_SPS_NUT, {sps_nal_unit.data}});
config.m_nal_array.push_back(HEVCDecoderConfigurationRecord::NalArray{0, HEVC_NAL_UNIT_PPS_NUT, {pps_nal_unit.data}});
@@ -769,8 +879,27 @@ static struct heif_error webcodecs_decode_image(void* decoder_raw,
emscripten::val memory_view(emscripten::typed_memory_view(len, buffer.get()));
memory_view.call<void>("set", js_array);
- const int width = result["codedWidth"].as<int>();
- const int height = result["codedHeight"].as<int>();
+ // These are the dimensions of the visible rectangle that the JavaScript side
+ // copied into 'buffer'. They are only used together with the plane layout
+ // after plane_fits_in_buffer() has confirmed that the layout stays within
+ // 'len' bytes.
+ if (result["width"].isUndefined() || result["height"].isUndefined()) {
+ return {heif_error_Decoder_plugin_error,
+ heif_suberror_Unspecified,
+ "Decoding failed: result.width or result.height is undefined"};
+ }
+
+ // The upper bound keeps the per-row byte counts computed below (up to four
+ // bytes per pixel) and the rounded-up chroma sizes representable as int.
+ const int max_dim = std::numeric_limits<int>::max() / 4;
+ const int width = result["width"].as<int>();
+ const int height = result["height"].as<int>();
+ if (width <= 0 || height <= 0 || width > max_dim || height > max_dim) {
+ return {heif_error_Decoder_plugin_error,
+ heif_suberror_Invalid_image_size,
+ "Decoding failed: invalid image size reported by the browser"};
+ }
+
std::string format = result["format"].as<std::string>();
emscripten::val planes = result["planes"];
@@ -820,7 +949,9 @@ static struct heif_error webcodecs_decode_image(void* decoder_raw,
uv_src_stride = uv_plane["stride"].as<int>();
}
- return convert_nv12_to_heif_image(buffer, width, height, y_offset, y_src_stride, uv_offset, uv_src_stride, out_img, (heif_chroma)config.chroma_format, is_full_range);
+ return convert_nv12_to_heif_image(buffer, len, width, height,
+ y_offset, y_src_stride, uv_offset, uv_src_stride,
+ out_img, is_mono, is_full_range, limits);
} else if (format == "I420" || format == "I422" || format == "I444") {
if (planes["length"].as<size_t>() < 3) {
return {heif_error_Decoder_plugin_error,
@@ -844,15 +975,28 @@ static struct heif_error webcodecs_decode_image(void* decoder_raw,
chroma = heif_chroma_444;
}
+ const int y_offset = y_plane["offset"].as<int>();
+ const int y_src_stride = y_plane["stride"].as<int>();
+ const int u_offset = u_plane["offset"].as<int>();
+ const int u_src_stride = u_plane["stride"].as<int>();
+ const int v_offset = v_plane["offset"].as<int>();
+ const int v_src_stride = v_plane["stride"].as<int>();
+
+ int chroma_w, chroma_h;
+ get_chroma_plane_size(chroma, width, height, &chroma_w, &chroma_h);
+
+ if (!plane_fits_in_buffer(len, y_offset, y_src_stride, height, width) ||
+ !plane_fits_in_buffer(len, u_offset, u_src_stride, chroma_h, chroma_w) ||
+ !plane_fits_in_buffer(len, v_offset, v_src_stride, chroma_h, chroma_w)) {
+ return kPlaneOutOfBoundsError;
+ }
+
return convert_planar_yuv_to_heif_image(
- buffer.get() + y_plane["offset"].as<int>(),
- y_plane["stride"].as<int>(),
- buffer.get() + u_plane["offset"].as<int>(),
- u_plane["stride"].as<int>(),
- buffer.get() + v_plane["offset"].as<int>(),
- v_plane["stride"].as<int>(),
+ buffer.get() + y_offset, y_src_stride,
+ buffer.get() + u_offset, u_src_stride,
+ buffer.get() + v_offset, v_src_stride,
width, height,
- out_img, chroma, is_full_range);
+ out_img, chroma, is_full_range, limits);
} else if (format == "RGBA") {
if (planes["length"].as<size_t>() < 1) {
return {heif_error_Decoder_plugin_error,
@@ -870,6 +1014,10 @@ static struct heif_error webcodecs_decode_image(void* decoder_raw,
const int rgba_offset = rgba_plane["offset"].as<int>();
const int rgba_src_stride = rgba_plane["stride"].as<int>();
+ if (!plane_fits_in_buffer(len, rgba_offset, rgba_src_stride, height, width * 4)) {
+ return kPlaneOutOfBoundsError;
+ }
+
heif_error err;
err = heif_image_create(width,
height,
@@ -880,7 +1028,7 @@ static struct heif_error webcodecs_decode_image(void* decoder_raw,
return err;
}
- err = heif_image_add_plane(*out_img, heif_channel_interleaved, width, height, 8);
+ err = heif_image_add_plane_safe(*out_img, heif_channel_interleaved, width, height, 8, limits);
if (err.code) {
heif_image_release(*out_img);
return err;
@@ -937,7 +1085,7 @@ static struct heif_error webcodecs_decode_next_image(void* decoder_raw,
struct heif_image** out_img,
const heif_security_limits* limits)
{
- return webcodecs_decode_image(decoder_raw, out_img);
+ return webcodecs_decode_image_with_limits(decoder_raw, out_img, limits);
}
@@ -949,7 +1097,15 @@ static struct heif_error webcodecs_decode_next_image2(void* decoder_raw,
if (out_user_data) {
*out_user_data = 0;
}
- return webcodecs_decode_image(decoder_raw, out_img);
+ return webcodecs_decode_image_with_limits(decoder_raw, out_img, limits);
+}
+
+
+static struct heif_error webcodecs_decode_image(void* decoder_raw,
+ struct heif_image** out_img)
+{
+ auto* limits = heif_get_global_security_limits();
+ return webcodecs_decode_image_with_limits(decoder_raw, out_img, limits);
}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 6dd22790..eb2c945c 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -55,6 +55,7 @@ else()
add_libheif_test(extract_area_plane_checks)
add_libheif_test(jpeg2000)
add_libheif_test(avc_box)
+ add_libheif_test(hevc_sps)
add_libheif_test(file_layout)
add_libheif_test(image_description_metadata)
add_libheif_test(exception_guard)
diff --git a/tests/hevc_sps.cc b/tests/hevc_sps.cc
new file mode 100644
index 00000000..c208e745
--- /dev/null
+++ b/tests/hevc_sps.cc
@@ -0,0 +1,84 @@
+/*
+ libheif HEVC SPS parsing unit tests
+
+ MIT License
+
+ Copyright (c) 2026 Dirk Farin <dirk.farin@gmail.com>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+#include "catch_amalgamated.hpp"
+#include "codecs/hevc_boxes.h"
+#include "codecs/decoder.h"
+#include "error.h"
+#include <cstdint>
+#include <vector>
+
+// SPS NAL unit (with emulation prevention bytes) taken from the hvcC box of
+// tests/data/rainbow-451x461.heic. x265 padded the 451x461 input to a coded
+// size of 456x464 and signals a conformance window that crops it to 452x462.
+static const std::vector<uint8_t> rainbow_sps{
+ 0x42, 0x01, 0x01, 0x03, 0x70, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00,
+ 0x03, 0x00, 0x00, 0x03, 0x00, 0x3f, 0xa0, 0x0e, 0x48, 0x07, 0x47, 0x75,
+ 0x96, 0xea, 0x49, 0x29, 0xae, 0x6e, 0x02, 0x1a, 0x0c, 0x08, 0x00, 0x00,
+ 0x03, 0x00, 0xc8, 0x00, 0x00, 0x03, 0x00, 0x08, 0x40};
+
+
+TEST_CASE("SPS conformance window yields visible and coded size")
+{
+ HEVCDecoderConfigurationRecord config;
+ uint32_t width = 0, height = 0;
+ ImageSize coded{};
+
+ Error err = parse_sps_for_hvcC_configuration(rainbow_sps.data(), rainbow_sps.size(),
+ &config, &width, &height, &coded);
+ REQUIRE(!err);
+
+ CHECK(config.chroma_format == 1);
+ CHECK(config.bit_depth_luma == 8);
+ CHECK(config.bit_depth_chroma == 8);
+
+ CHECK(width == 452);
+ CHECK(height == 462);
+ CHECK(coded.width == 456);
+ CHECK(coded.height == 464);
+}
+
+
+TEST_CASE("SPS chroma_format_idc out of range is rejected")
+{
+ // Same SPS with chroma_format_idc changed from 1 (uvlc '010') to 4
+ // (uvlc '00101'). The byte at index 18 holds sps_seq_parameter_set_id and
+ // the start of chroma_format_idc: '1 010 ....' becomes '1 00101 ..'.
+ std::vector<uint8_t> sps = rainbow_sps;
+ REQUIRE((sps[18] & 0xF0) == 0xA0);
+ sps[18] = 0x94 | (sps[18] & 0x03);
+
+ HEVCDecoderConfigurationRecord config;
+ uint32_t width = 0, height = 0;
+ ImageSize coded{};
+
+ Error err = parse_sps_for_hvcC_configuration(sps.data(), sps.size(),
+ &config, &width, &height, &coded);
+ REQUIRE(err);
+ CHECK(err.error_code == heif_error_Invalid_input);
+ CHECK(err.sub_error_code == heif_suberror_Invalid_parameter_value);
+ CHECK(err.message.find("chroma_format_idc") != std::string::npos);
+}