Commit d9862918 for libheif
commit d9862918f9966f037a2125db22fb27df6ddab8c1
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon Sep 21 14:37:58 2026 +0200
Test ImageOverlay offset decoding over the full 16-bit and a sampled 32-bit range
The sign extension bug fixed in f313930b (16-bit offsets decoded as if
they were 32-bit two's complement) survived for several releases because
the offset fields were only checked indirectly and only for a few values.
Check ImageOverlay::parse() directly for every 16-bit field value and for
the edge values plus 65536 evenly spread samples of the 32-bit range, and
check the write()/parse() round trip including the automatic switch to
32-bit fields. The old code fails the 16-bit and round-trip cases.
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 08e5b50a..b2a9c7f5 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -57,6 +57,7 @@ else()
add_libheif_test(extract_area_plane_checks)
add_libheif_test(extend_to_size_checks)
add_libheif_test(pixelimage_overlay)
+ add_libheif_test(overlay_spec_parse)
add_libheif_test(add_channel_checks)
add_libheif_test(jpeg2000)
add_libheif_test(avc_box)
diff --git a/tests/overlay_spec_parse.cc b/tests/overlay_spec_parse.cc
new file mode 100644
index 00000000..a51b7ce2
--- /dev/null
+++ b/tests/overlay_spec_parse.cc
@@ -0,0 +1,206 @@
+/*
+ libheif 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.
+*/
+
+// ImageOverlay::parse() reads the canvas size and the per-image offsets from
+// 2-byte fields, or from 4-byte fields when bit 0 of the flags byte is set
+// (ISO/IEC 23008-12 6.6.2.2). The offsets are signed two's complement values.
+//
+// A regression in v1.19.2 (ab0565a6) sign-extended the 2-byte offsets as if
+// they were 4 bytes wide, so every negative 16-bit offset came out as about
+// -2^31 and the image was silently dropped from the canvas. That went unnoticed
+// for a long time because the sign extension was only checked indirectly and
+// only for a few values. tests/overlay_offsets.cc covers a handful of offsets
+// through the full decode path. This file checks the field decoding itself over
+// the complete 16-bit range and a broad sample of the 32-bit range, plus the
+// write()/parse() round trip including the automatic field-size selection.
+
+#include "image-items/overlay.h"
+#include "catch_amalgamated.hpp"
+
+#include <climits>
+#include <cstdint>
+#include <vector>
+
+
+namespace {
+
+void put_be(std::vector<uint8_t>& out, uint32_t v, int len)
+{
+ for (int i = len - 1; i >= 0; i--) {
+ out.push_back(static_cast<uint8_t>(v >> (8 * i)));
+ }
+}
+
+// ImageOverlay payload: version 0, flags, black background, a 1x1 canvas and
+// then the raw offset fields in file order (x0, y0, x1, y1, ...).
+std::vector<uint8_t> make_spec(bool long_fields, const std::vector<uint32_t>& raw_offset_fields)
+{
+ const int len = long_fields ? 4 : 2;
+
+ std::vector<uint8_t> s;
+ s.push_back(0); // version
+ s.push_back(long_fields ? 1 : 0); // flags
+ for (int i = 0; i < 4; i++) {
+ put_be(s, 0, 2); // background RGBA
+ }
+ put_be(s, 1, len); // canvas width
+ put_be(s, 1, len); // canvas height
+ for (uint32_t f : raw_offset_fields) {
+ put_be(s, f, len);
+ }
+
+ return s;
+}
+
+// Parse one overlay holding all 'values' as x offsets and their bitwise
+// complements as y offsets, and compare each decoded pair with the expected
+// two's complement interpretation. Using the complement for y makes a wrong
+// field order or a wrong read-pointer advance visible as well.
+void check_offset_fields(bool long_fields, const std::vector<uint32_t>& values)
+{
+ std::vector<uint32_t> fields;
+ fields.reserve(2 * values.size());
+
+ const uint32_t field_mask = long_fields ? 0xFFFFFFFFu : 0xFFFFu;
+
+ for (uint32_t v : values) {
+ fields.push_back(v);
+ fields.push_back(~v & field_mask);
+ }
+
+ ImageOverlay ovl;
+ Error err = ovl.parse(values.size(), make_spec(long_fields, fields));
+ INFO("parse error: " << err.message);
+ REQUIRE(!err);
+ REQUIRE(ovl.get_num_offsets() == values.size());
+
+ for (size_t i = 0; i < values.size(); i++) {
+ const uint32_t v = values[i];
+ const uint32_t v_compl = ~v & field_mask;
+
+ const int32_t expected_x = long_fields ? static_cast<int32_t>(v) : static_cast<int16_t>(v);
+ const int32_t expected_y = long_fields ? static_cast<int32_t>(v_compl) : static_cast<int16_t>(v_compl);
+
+ int32_t x = 0, y = 0;
+ ovl.get_offset(i, &x, &y);
+
+ // A plain REQUIRE per value would work but bloats the assertion count.
+ if (x != expected_x || y != expected_y) {
+ FAIL((long_fields ? 32 : 16) << "-bit field 0x" << std::hex << v << std::dec
+ << " decoded as (" << x << "," << y << "), expected ("
+ << expected_x << "," << expected_y << ")");
+ }
+ }
+
+ SUCCEED();
+}
+
+} // namespace
+
+
+TEST_CASE("overlay spec: every 16-bit offset value")
+{
+ std::vector<uint32_t> values;
+ values.reserve(0x10000);
+ for (uint32_t v = 0; v <= 0xFFFF; v++) {
+ values.push_back(v);
+ }
+
+ check_offset_fields(false, values);
+}
+
+
+TEST_CASE("overlay spec: 32-bit offsets over the edges and a spread sample of the range")
+{
+ std::vector<uint32_t> values = {
+ 0, 1, 0x7FFF, 0x8000, 0xFFFF, 0x10000,
+ 0x7FFFFFFF, 0x80000000, 0x80000001,
+ 0xFFFF0000, 0xFFFF7FFF, 0xFFFF8000,
+ 0xFFFFFD80, // -640, the C021 offset
+ 0xFFFFFFFE, 0xFFFFFFFF,
+ };
+
+ // Multiples of an odd constant wrap around 2^32 and spread evenly over the
+ // range, so every high byte and both signs are visited many times.
+ uint32_t v = 0;
+ for (int i = 0; i < 0x10000; i++) {
+ v += 0x9E3779B1u;
+ values.push_back(v);
+ }
+
+ check_offset_fields(true, values);
+}
+
+
+TEST_CASE("overlay spec: write() and parse() round trip with automatic field size")
+{
+ struct Case
+ {
+ uint32_t canvas_w, canvas_h;
+ int32_t x, y;
+ bool expect_long_fields;
+ };
+
+ const Case cases[] = {
+ {640, 360, 0, 0, false},
+ {640, 360, -640, -360, false}, // C021
+ {640, 360, 32767, -32768, false}, // 16-bit extremes still fit
+ {640, 360, 32768, 0, true}, // one past the positive 16-bit limit
+ {640, 360, 0, -32769, true}, // one past the negative 16-bit limit
+ {640, 360, INT32_MAX, INT32_MIN, true},
+ {0xFFFF, 0xFFFF, -1, -1, false}, // largest 16-bit canvas
+ {0x10000, 1, -1, -1, true}, // canvas forces long fields
+ };
+
+ for (const Case& c : cases) {
+ INFO("canvas " << c.canvas_w << "x" << c.canvas_h << ", offset (" << c.x << "," << c.y << ")");
+
+ ImageOverlay ovl;
+ ovl.set_canvas_size(c.canvas_w, c.canvas_h);
+ ovl.add_image_on_top(1, c.x, c.y);
+
+ std::vector<uint8_t> data = ovl.write();
+
+ const size_t field_len = c.expect_long_fields ? 4 : 2;
+ REQUIRE(data.size() == 2 + 4 * 2 + field_len * 4);
+ REQUIRE(data[0] == 0);
+ REQUIRE(data[1] == (c.expect_long_fields ? 1 : 0));
+
+ ImageOverlay parsed;
+ Error err = parsed.parse(1, data);
+ INFO("parse error: " << err.message);
+ REQUIRE(!err);
+
+ REQUIRE(parsed.get_canvas_width() == c.canvas_w);
+ REQUIRE(parsed.get_canvas_height() == c.canvas_h);
+ REQUIRE(parsed.get_num_offsets() == 1);
+
+ int32_t x = 0, y = 0;
+ parsed.get_offset(0, &x, &y);
+ REQUIRE(x == c.x);
+ REQUIRE(y == c.y);
+ }
+}