Commit ea536da3 for libheif
commit ea536da34ea423ed4daa47fac0801bc6b93a00ad
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Sun Oct 25 16:38:46 2026 +0100
Bound sequence decode work without output [GHSA-73j7-gvm9-x8q6]
diff --git a/libheif/sequences/track_visual.cc b/libheif/sequences/track_visual.cc
index 4efda475..258b507e 100644
--- a/libheif/sequences/track_visual.cc
+++ b/libheif/sequences/track_visual.cc
@@ -32,6 +32,35 @@
#include "codecs/uncompressed/unc_boxes.h"
+namespace {
+
+// One outstanding sample is enough for codecs without frame reordering: after
+// submitting a complete sample, the next poll must return an image or an error.
+// Video codecs may buffer pictures. This is a work budget, not a codec latency
+// claim: each returned frame also pays for one non-displayed sample.
+uint16_t max_sequence_decode_work_debt(heif_compression_format format)
+{
+ switch (format) {
+ case heif_compression_JPEG:
+ case heif_compression_JPEG2000:
+ case heif_compression_HTJ2K:
+ case heif_compression_uncompressed:
+ case heif_compression_mask:
+ return 1;
+ default:
+ return 32;
+ }
+}
+
+Error sequence_decode_work_limit_error()
+{
+ return {heif_error_Invalid_input, heif_suberror_Security_limit_exceeded,
+ "Sequence decoding exceeded the limit for samples without enough output frames."};
+}
+
+} // namespace
+
+
Track_Visual::Track_Visual(HeifContext* ctx)
: Track(ctx)
{
@@ -193,6 +222,10 @@ bool Track_Visual::has_alpha_channel() const
Result<std::shared_ptr<HeifPixelImage> > Track_Visual::decode_next_image_sample(const heif_decoding_options& options)
{
+ if (m_decode_work_limit_exceeded) {
+ return sequence_decode_work_limit_error();
+ }
+
// --- If we ignore the editlist, we stop when we reached the end of the original samples.
uint64_t num_output_samples = m_num_output_samples;
@@ -223,6 +256,10 @@ Result<std::shared_ptr<HeifPixelImage> > Track_Visual::decode_next_image_sample(
const std::shared_ptr<Chunk>& chunk = m_chunks[chunk_idx];
+ // TODO: A sample-description change can switch codecs (e.g. HEVC to JPEG 2000).
+ // Before using the new decoder, collect any pending output from the previous
+ // decoder in presentation order. Selecting a decoder by the next input sample
+ // alone does not poll the previous decoder after the switch.
auto decoder = chunk->get_decoder();
if (!decoder) {
// Should not happen: Track_Visual::load() rejects any visual track that has a
@@ -236,6 +273,9 @@ Result<std::shared_ptr<HeifPixelImage> > Track_Visual::decode_next_image_sample(
};
}
+ const heif_compression_format format = decoder->get_compression_format();
+ uint16_t& decode_work_debt = m_decode_work_debt[format];
+
// avoid calling get_decoded_frame() before starting the decoder.
if (m_next_sample_to_be_decoded != 0) {
Result<std::shared_ptr<HeifPixelImage> > getFrameResult = decoder->get_decoded_frame(options,
@@ -251,6 +291,10 @@ Result<std::shared_ptr<HeifPixelImage> > Track_Visual::decode_next_image_sample(
if (*getFrameResult != nullptr) {
image = *getFrameResult;
+ // Credit the returned frame and one potentially non-displayed sample.
+ // Do this at the decoder boundary, even if later image postprocessing fails.
+ decode_work_debt = (decode_work_debt > 2) ? decode_work_debt - 2 : 0;
+
// If this was the last frame in the EditList segment, reset the 'flushed' flag
// in case we have to restart the decoder for another repetition of the segment.
if ((m_next_sample_to_be_output + 1) % m_presentation_timeline.size() == 0) {
@@ -272,7 +316,12 @@ Result<std::shared_ptr<HeifPixelImage> > Track_Visual::decode_next_image_sample(
// --- Push more data into the decoder (or send end-of-sequence).
- if (m_next_sample_to_be_decoded < m_num_output_samples) {
+ if (m_next_sample_to_be_decoded < num_output_samples) {
+
+ if (decode_work_debt >= max_sequence_decode_work_debt(format)) {
+ m_decode_work_limit_exceeded = true;
+ return sequence_decode_work_limit_error();
+ }
// --- Find the data extent that stores the compressed frame data.
@@ -296,6 +345,8 @@ Result<std::shared_ptr<HeifPixelImage> > Track_Visual::decode_next_image_sample(
if (decodingError) {
return decodingError;
}
+
+ decode_work_debt++;
}
else {
// --- End of sequence (Editlist segment) reached.
diff --git a/libheif/sequences/track_visual.h b/libheif/sequences/track_visual.h
index bb683089..0cff7c96 100644
--- a/libheif/sequences/track_visual.h
+++ b/libheif/sequences/track_visual.h
@@ -96,6 +96,11 @@ private:
heif_encoder* m_active_encoder = nullptr;
std::unique_ptr<heif_encoder> m_alpha_track_encoder;
+ // Separate amortized work budgets for each compression format. A frame credits
+ // its own format only; balances saturate at zero and persist across decode calls.
+ std::map<heif_compression_format, uint16_t> m_decode_work_debt;
+ bool m_decode_work_limit_exceeded = false;
+
Result<bool> process_encoded_data(heif_encoder* encoder);
};
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 7a52053e..98bd28dc 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -145,6 +145,7 @@ add_libheif_test(sequence_vfr_durations)
add_libheif_test(track_reference_duplicates)
add_libheif_test(sequence_raw_sample_errors)
add_libheif_test(sequence_nonvisual_sample_entry)
+add_libheif_test(sequence_no_decoder_output)
add_libheif_test(tai)
add_libheif_test(text)
add_libheif_test(cxx_wrapper)
diff --git a/tests/sequence_file_builder.h b/tests/sequence_file_builder.h
index 8ed566c4..3a0f5344 100644
--- a/tests/sequence_file_builder.h
+++ b/tests/sequence_file_builder.h
@@ -24,10 +24,9 @@
SOFTWARE.
*/
-// Builds minimal, structurally valid HEIF sequence files containing a single
-// 'urim' metadata track, following the box tree that Track::load() requires.
-// A metadata track is used so that the raw-sample API path can be exercised
-// without any codec plugin. Shared by the sequence_* tests.
+// Builds minimal, structurally valid HEIF sequence files following the box
+// tree that Track::load() requires. Defaults to a 'urim' metadata track for
+// raw-sample API tests; visual-track tests can override its sample entries.
#ifndef LIBHEIF_TESTS_SEQUENCE_FILE_BUILDER_H
#define LIBHEIF_TESTS_SEQUENCE_FILE_BUILDER_H
@@ -124,12 +123,22 @@ struct SequenceFileParams
uint32_t num_samples = 1;
uint32_t sample_size = 1;
+ std::vector<uint8_t> sample_bytes; // if set, repeat these bytes for every sample
+
+ // Override these two to build a track of a different kind, e.g. a 'pict' track with 'hvc1' samples.
+ const char* handler_type = "meta";
+ std::vector<uint8_t> sample_entry; // complete sample entry box, empty -> 'urim'
+ uint32_t first_chunk_num_samples = 0; // nonzero -> split into two chunks
+ std::vector<uint8_t> second_sample_entry;
+ std::vector<uint8_t> second_sample_bytes; // same length as sample_bytes
std::vector<SampleAuxInfo> aux_infos;
};
inline std::vector<uint8_t> build_sequence_file(const SequenceFileParams& p)
{
+ const uint32_t sample_size = p.sample_bytes.empty() ? p.sample_size : static_cast<uint32_t>(p.sample_bytes.size());
+ const bool two_chunks = p.first_chunk_num_samples != 0;
// --- ftyp
std::vector<uint8_t> ftyp_payload;
put_fourcc(ftyp_payload, "msf1"); // major brand: HEIF image sequence
@@ -206,11 +215,11 @@ inline std::vector<uint8_t> build_sequence_file(const SequenceFileParams& p)
put16(mdhd_payload, 0); // pre_defined
std::vector<uint8_t> mdhd = box("mdhd", mdhd_payload);
- // --- hdlr (handler type 'meta')
+ // --- hdlr (handler type 'meta' by default)
std::vector<uint8_t> hdlr_payload;
put32(hdlr_payload, 0x00000000); // version 0, flags 0
put32(hdlr_payload, 0); // pre_defined
- put_fourcc(hdlr_payload, "meta");
+ put_fourcc(hdlr_payload, p.handler_type);
put32(hdlr_payload, 0); // reserved
put32(hdlr_payload, 0); // reserved
put32(hdlr_payload, 0); // reserved
@@ -222,7 +231,7 @@ inline std::vector<uint8_t> build_sequence_file(const SequenceFileParams& p)
put32(nmhd_payload, 0x00000000); // version 0, flags 0
std::vector<uint8_t> nmhd = box("nmhd", nmhd_payload);
- // --- stsd with a single 'urim' (URI meta) sample entry
+ // --- stsd with one or two sample entries ('urim', URI meta, by default)
std::vector<uint8_t> urim_payload;
for (int i = 0; i < 6; i++) urim_payload.push_back(0); // SampleEntry reserved
put16(urim_payload, 1); // data_reference_index
@@ -230,8 +239,9 @@ inline std::vector<uint8_t> build_sequence_file(const SequenceFileParams& p)
std::vector<uint8_t> stsd_payload;
put32(stsd_payload, 0x00000000); // version 0, flags 0
- put32(stsd_payload, 1); // entry_count
- append(stsd_payload, urim);
+ put32(stsd_payload, two_chunks ? 2 : 1); // entry_count
+ append(stsd_payload, p.sample_entry.empty() ? urim : p.sample_entry);
+ if (two_chunks) append(stsd_payload, p.second_sample_entry);
std::vector<uint8_t> stsd = box("stsd", stsd_payload);
// --- stts
@@ -244,19 +254,24 @@ inline std::vector<uint8_t> build_sequence_file(const SequenceFileParams& p)
}
std::vector<uint8_t> stts = box("stts", stts_payload);
- // --- stsc: all samples in a single chunk
+ // --- stsc: one or two chunks, each with its own sample description
std::vector<uint8_t> stsc_payload;
put32(stsc_payload, 0x00000000); // version 0, flags 0
- put32(stsc_payload, 1); // entry_count
+ put32(stsc_payload, two_chunks ? 2 : 1); // entry_count
put32(stsc_payload, 1); // first_chunk
- put32(stsc_payload, p.num_samples); // samples_per_chunk
+ put32(stsc_payload, two_chunks ? p.first_chunk_num_samples : p.num_samples); // samples_per_chunk
put32(stsc_payload, 1); // sample_description_index
+ if (two_chunks) {
+ put32(stsc_payload, 2); // first_chunk
+ put32(stsc_payload, p.num_samples - p.first_chunk_num_samples);
+ put32(stsc_payload, 2); // sample_description_index
+ }
std::vector<uint8_t> stsc = box("stsc", stsc_payload);
// --- stsz: fixed sample size
std::vector<uint8_t> stsz_payload;
put32(stsz_payload, 0x00000000); // version 0, flags 0
- put32(stsz_payload, p.sample_size); // fixed sample size (non-zero -> no per-sample array)
+ put32(stsz_payload, sample_size); // fixed sample size (non-zero -> no per-sample array)
put32(stsz_payload, p.num_samples); // sample_count
std::vector<uint8_t> stsz = box("stsz", stsz_payload);
@@ -273,11 +288,11 @@ inline std::vector<uint8_t> build_sequence_file(const SequenceFileParams& p)
}
// --- stco / saio: offsets are patched below to point into the mdat payload
- auto make_stco = [](uint32_t offset) {
+ auto make_stco = [](const std::vector<uint32_t>& offsets) {
std::vector<uint8_t> stco_payload;
put32(stco_payload, 0x00000000); // version 0, flags 0
- put32(stco_payload, 1); // entry_count
- put32(stco_payload, offset); // chunk offset
+ put32(stco_payload, static_cast<uint32_t>(offsets.size())); // entry_count
+ for (uint32_t offset : offsets) put32(stco_payload, offset);
return box("stco", stco_payload);
};
@@ -306,9 +321,12 @@ inline std::vector<uint8_t> build_sequence_file(const SequenceFileParams& p)
// --- mdat payload: num_samples * sample_size bytes of arbitrary content,
// followed by one block of aux-info payloads per saiz/saio pair.
- std::vector<uint8_t> mdat_payload(static_cast<size_t>(p.num_samples) * p.sample_size);
+ std::vector<uint8_t> mdat_payload(static_cast<size_t>(p.num_samples) * sample_size);
for (size_t i = 0; i < mdat_payload.size(); i++) {
- mdat_payload[i] = static_cast<uint8_t>(0xA0 + (i & 0x0f));
+ const auto& sample_bytes = (two_chunks && i >= static_cast<size_t>(p.first_chunk_num_samples) * sample_size)
+ ? p.second_sample_bytes : p.sample_bytes;
+ mdat_payload[i] = sample_bytes.empty() ? static_cast<uint8_t>(0xA0 + (i & 0x0f))
+ : sample_bytes[i % sample_size];
}
std::vector<size_t> aux_block_offsets; // relative to the start of the mdat payload
@@ -325,7 +343,8 @@ inline std::vector<uint8_t> build_sequence_file(const SequenceFileParams& p)
for (const auto& aux : p.aux_infos) {
saio_placeholders.push_back(make_saio(aux, 0));
}
- std::vector<uint8_t> moov0 = assemble(make_stco(0), saio_placeholders);
+ std::vector<uint8_t> moov0 = assemble(make_stco(two_chunks ? std::vector<uint32_t>{0, 0}
+ : std::vector<uint32_t>{0}), saio_placeholders);
uint32_t mdat_payload_offset = static_cast<uint32_t>(ftyp.size() + moov0.size() + 8 /* mdat header */);
uint32_t file_size = static_cast<uint32_t>(mdat_payload_offset + mdat_payload.size());
@@ -336,7 +355,9 @@ inline std::vector<uint8_t> build_sequence_file(const SequenceFileParams& p)
: mdat_payload_offset + static_cast<uint32_t>(aux_block_offsets[i]);
saio_boxes.push_back(make_saio(p.aux_infos[i], offset));
}
- std::vector<uint8_t> moov = assemble(make_stco(mdat_payload_offset), saio_boxes);
+ std::vector<uint32_t> chunk_offsets = {mdat_payload_offset};
+ if (two_chunks) chunk_offsets.push_back(mdat_payload_offset + p.first_chunk_num_samples * sample_size);
+ std::vector<uint8_t> moov = assemble(make_stco(chunk_offsets), saio_boxes);
std::vector<uint8_t> mdat = box("mdat", mdat_payload);
diff --git a/tests/sequence_no_decoder_output.cc b/tests/sequence_no_decoder_output.cc
new file mode 100644
index 00000000..55391975
--- /dev/null
+++ b/tests/sequence_no_decoder_output.cc
@@ -0,0 +1,518 @@
+/*
+ libheif regression test: a visual sequence track whose samples contain no picture,
+ combined with an edit list in "repeat" mode. The decoder accepts the samples, but it
+ never outputs a frame. A single heif_track_decode_next_image() call then pushed the
+ same samples again and again, up to max_sequence_frames times, before it gave up.
+
+ 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 "libheif/heif.h"
+#include "libheif/heif_plugin.h"
+#include "libheif/heif_sequences.h"
+#include "sequence_file_builder.h"
+
+#include <cstdint>
+#include <limits>
+#include <vector>
+
+using namespace seqfile;
+
+namespace {
+
+// --- A mock HEVC decoder plugin, so that the test does not depend on the installed codecs
+// and can count how often data is pushed. It ignores the data. It outputs one frame for
+// each 'mock_output_divisor' pushed samples after an initial latency, or no frames at all.
+
+const int no_output = -1;
+
+int mock_latency = no_output;
+int mock_output_divisor = 1;
+int mock_output_multiplier = 1;
+int mock_num_pushed = 0;
+int mock_num_output = 0;
+bool mock_flushed = false;
+
+const heif_error mock_ok = {heif_error_Ok, heif_suberror_Unspecified, "Success"};
+
+const char* mock_plugin_name() { return "mock decoder"; }
+
+int mock_does_support_format(heif_compression_format format)
+{
+ return (format == heif_compression_HEVC || format == heif_compression_JPEG) ? 1000 : 0;
+}
+
+int mock_does_support_format2(const heif_decoder_plugin_compressed_format_description* desc)
+{
+ return mock_does_support_format(desc->format);
+}
+
+heif_error mock_new_decoder(void** decoder)
+{
+ *decoder = &mock_num_pushed; // any non-NULL pointer
+ return mock_ok;
+}
+
+heif_error mock_new_decoder2(void** decoder, const heif_decoder_plugin_options*)
+{
+ return mock_new_decoder(decoder);
+}
+
+void mock_free_decoder(void*) {}
+
+heif_error mock_push_data2(void*, const void*, size_t, uintptr_t)
+{
+ mock_num_pushed++;
+ return mock_ok;
+}
+
+heif_error mock_push_data(void* decoder, const void* data, size_t size)
+{
+ return mock_push_data2(decoder, data, size, 0);
+}
+
+heif_error mock_flush_data(void*)
+{
+ mock_flushed = true;
+ return mock_ok;
+}
+
+heif_error mock_decode_next_image2(void*, heif_image** out_img, uintptr_t* out_user_data, const heif_security_limits*)
+{
+ *out_img = nullptr;
+
+ int latency = mock_flushed ? 0 : mock_latency;
+ if (mock_latency == no_output || mock_num_pushed <= latency ||
+ mock_num_output >= (mock_num_pushed - latency) * mock_output_multiplier / mock_output_divisor) {
+ return mock_ok;
+ }
+
+ heif_error err = heif_image_create(4, 4, heif_colorspace_monochrome, heif_chroma_monochrome, out_img);
+ if (err.code) {
+ return err;
+ }
+
+ err = heif_image_add_plane(*out_img, heif_channel_Y, 4, 4, 8);
+ if (err.code) {
+ return err;
+ }
+
+ if (out_user_data) {
+ *out_user_data = 0;
+ }
+
+ mock_num_output++;
+ return mock_ok;
+}
+
+heif_error mock_decode_next_image(void* decoder, heif_image** out_img, const heif_security_limits* limits)
+{
+ return mock_decode_next_image2(decoder, out_img, nullptr, limits);
+}
+
+heif_error mock_decode_image(void* decoder, heif_image** out_img)
+{
+ return mock_decode_next_image2(decoder, out_img, nullptr, nullptr);
+}
+
+void mock_set_strict_decoding(void*, int) {}
+
+const heif_decoder_plugin mock_decoder_plugin{
+ 5,
+ mock_plugin_name,
+ nullptr, // init_plugin
+ nullptr, // deinit_plugin
+ mock_does_support_format,
+ mock_new_decoder,
+ mock_free_decoder,
+ mock_push_data,
+ mock_decode_image,
+ mock_set_strict_decoding,
+ "mock",
+ mock_decode_next_image,
+ LIBHEIF_MAKE_VERSION(1, 21, 0),
+ mock_does_support_format2,
+ mock_new_decoder2,
+ mock_push_data2,
+ mock_flush_data,
+ mock_decode_next_image2
+};
+
+void start_mock_decoder(int latency, int output_divisor = 1, int output_multiplier = 1)
+{
+ static bool registered = false;
+ if (!registered) {
+ REQUIRE(heif_register_decoder_plugin(&mock_decoder_plugin).code == heif_error_Ok);
+ registered = true;
+ }
+
+ mock_latency = latency;
+ mock_output_divisor = output_divisor;
+ mock_output_multiplier = output_multiplier;
+ mock_num_pushed = 0;
+ mock_num_output = 0;
+ mock_flushed = false;
+}
+
+
+// A minimal visual sample entry. For HEVC, include an hvcC box without parameter sets.
+std::vector<uint8_t> make_visual_entry(const char* type, bool with_hevc_config)
+{
+ std::vector<uint8_t> vse;
+ for (int i = 0; i < 6; i++) vse.push_back(0); // SampleEntry reserved
+ put16(vse, 1); // data_reference_index
+ put16(vse, 0); // pre_defined
+ put16(vse, 0); // reserved
+ for (int i = 0; i < 3; i++) put32(vse, 0); // pre_defined
+ put16(vse, 4); // width
+ put16(vse, 4); // height
+ put32(vse, 0x00480000); // horizresolution 72 dpi
+ put32(vse, 0x00480000); // vertresolution 72 dpi
+ put32(vse, 0); // reserved
+ put16(vse, 1); // frame_count
+ for (int i = 0; i < 32; i++) vse.push_back(0);// compressorname (length-prefixed, empty)
+ put16(vse, 0x0018); // depth
+ put16(vse, 0xFFFF); // pre_defined (-1)
+
+ if (with_hevc_config) {
+ // HEVCDecoderConfigurationRecord, 23 bytes, zero NAL arrays.
+ std::vector<uint8_t> hvcc;
+ hvcc.push_back(1); // configuration_version
+ hvcc.push_back(0); // profile_space / tier / profile_idc
+ put32(hvcc, 0); // general_profile_compatibility_flags
+ for (int i = 0; i < 6; i++) hvcc.push_back(0); // general_constraint_indicator_flags
+ hvcc.push_back(0); // general_level_idc
+ put16(hvcc, 0); // min_spatial_segmentation_idc
+ hvcc.push_back(0); // parallelism_type
+ hvcc.push_back(1); // chroma_format (4:2:0)
+ hvcc.push_back(0); // bit_depth_luma_minus8
+ hvcc.push_back(0); // bit_depth_chroma_minus8
+ put16(hvcc, 0); // avg_frame_rate
+ hvcc.push_back(3); // constant_frame_rate / temporal layers / length_size
+ hvcc.push_back(0); // num_of_arrays
+ append(vse, box("hvcC", hvcc));
+ }
+
+ return box(type, vse);
+}
+
+// A 'pict' track with 'num_samples' samples that is repeated endlessly.
+std::vector<uint8_t> build_endless_visual_sequence(uint32_t num_samples)
+{
+ SequenceFileParams p;
+ p.mvhd_duration = std::numeric_limits<uint64_t>::max(); // indefinite sentinel
+ p.mdhd_duration = num_samples;
+ p.elst_segment_duration = num_samples;
+ p.stts = {{num_samples, 1}};
+ p.num_samples = num_samples;
+ p.handler_type = "pict";
+ p.sample_entry = make_visual_entry("hvc1", true);
+ // A length-prefixed HEVC access unit containing only an AUD, no picture.
+ p.sample_bytes = {0, 0, 0, 2, 0x46, 0x01};
+
+ return build_sequence_file(p);
+}
+
+std::vector<uint8_t> build_endless_jpeg_sequence()
+{
+ SequenceFileParams p;
+ p.mvhd_duration = std::numeric_limits<uint64_t>::max();
+ p.mdhd_duration = 1;
+ p.elst_segment_duration = 1;
+ p.stts = {{1, 1}};
+ p.handler_type = "pict";
+ p.sample_entry = make_visual_entry("mjpg", false);
+ // Enough JPEG SOF data for the coded-size check; the mock decoder ignores it.
+ p.sample_bytes = {0xff, 0xd8, 0xff, 0xc0, 0x00, 0x0b, 0x08,
+ 0x00, 0x04, 0x00, 0x04, 0x01, 0x01, 0x11,
+ 0x00, 0xff, 0xd9};
+ return build_sequence_file(p);
+}
+
+std::vector<uint8_t> build_endless_hevc_then_jpeg_sequence()
+{
+ SequenceFileParams p;
+ p.mvhd_duration = std::numeric_limits<uint64_t>::max();
+ p.mdhd_duration = 12;
+ p.elst_segment_duration = 12;
+ p.stts = {{12, 1}};
+ p.num_samples = 12;
+ p.first_chunk_num_samples = 10;
+ p.handler_type = "pict";
+ p.sample_entry = make_visual_entry("hvc1", true);
+ p.second_sample_entry = make_visual_entry("mjpg", false);
+ // Both sample descriptions use 17-byte samples because stsz has a fixed size.
+ p.sample_bytes = {0, 0, 0, 13, 0x46, 0x01, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0};
+ p.second_sample_bytes = {0xff, 0xd8, 0xff, 0xc0, 0x00, 0x0b, 0x08,
+ 0x00, 0x04, 0x00, 0x04, 0x01, 0x01, 0x11,
+ 0x00, 0xff, 0xd9};
+ return build_sequence_file(p);
+}
+
+} // namespace
+
+
+TEST_CASE("repeated samples without decoder output are not pushed endlessly")
+{
+ const uint32_t num_samples = 3;
+ std::vector<uint8_t> file = build_endless_visual_sequence(num_samples);
+
+ start_mock_decoder(no_output);
+
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+
+ // Lower the frame limit (default: 18 million) so that a regression fails quickly.
+ heif_context_get_security_limits(ctx)->max_sequence_frames = 100000;
+
+ heif_error err = heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_track* track = heif_context_get_track(ctx, 0);
+ REQUIRE(track != nullptr);
+ REQUIRE(heif_track_get_number_of_repetitions(track) ==
+ heif_sequence_track_number_of_repetitions_infinite);
+
+ heif_image* img = nullptr;
+ err = heif_track_decode_next_image(track, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr);
+ INFO("error: " << err.message << ", subcode: " << err.subcode << ", pushed: " << mock_num_pushed);
+ REQUIRE(err.code == heif_error_Invalid_input);
+ REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded);
+ REQUIRE(img == nullptr);
+
+ // The decoder has seen all samples, but only a small number of repetitions.
+ // Before the fix, this was max_sequence_frames.
+ REQUIRE(mock_num_pushed == 32);
+
+ // Asking again does not start over.
+ int num_pushed_in_first_call = mock_num_pushed;
+ err = heif_track_decode_next_image(track, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr);
+ REQUIRE(err.code == heif_error_Invalid_input);
+ REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded);
+ REQUIRE(mock_num_pushed == num_pushed_in_first_call);
+
+ heif_track_release(track);
+ heif_context_free(ctx);
+}
+
+
+TEST_CASE("JPEG sequence stops after one sample without output")
+{
+ std::vector<uint8_t> file = build_endless_jpeg_sequence();
+ start_mock_decoder(no_output);
+
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+ REQUIRE(heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr).code == heif_error_Ok);
+ heif_track* track = heif_context_get_track(ctx, 0);
+ REQUIRE(track != nullptr);
+
+ heif_image* img = nullptr;
+ heif_error err = heif_track_decode_next_image(track, &img, heif_colorspace_undefined,
+ heif_chroma_undefined, nullptr);
+ REQUIRE(err.code == heif_error_Invalid_input);
+ REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded);
+ REQUIRE(img == nullptr);
+ REQUIRE(mock_num_pushed == 1);
+
+ heif_track_release(track);
+ heif_context_free(ctx);
+}
+
+
+TEST_CASE("HEVC debt does not exhaust the JPEG budget at a codec change")
+{
+ std::vector<uint8_t> file = build_endless_hevc_then_jpeg_sequence();
+ start_mock_decoder(no_output);
+
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+ REQUIRE(heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr).code == heif_error_Ok);
+ heif_track* track = heif_context_get_track(ctx, 0);
+ REQUIRE(track != nullptr);
+
+ heif_image* img = nullptr;
+ heif_error err = heif_track_decode_next_image(track, &img, heif_colorspace_undefined,
+ heif_chroma_undefined, nullptr);
+ REQUIRE(err.code == heif_error_Invalid_input);
+ REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded);
+ REQUIRE(img == nullptr);
+ // Ten HEVC samples and one JPEG sample are accepted. The next JPEG sample
+ // exceeds JPEG's own budget of one; HEVC's outstanding debt is not charged to it.
+ REQUIRE(mock_num_pushed == 11);
+
+ heif_track_release(track);
+ heif_context_free(ctx);
+}
+
+
+TEST_CASE("decoder latency may exceed the length of the repeated segment")
+{
+ // The first frame comes out only after the single sample was repeated many times.
+ std::vector<uint8_t> file = build_endless_visual_sequence(1);
+
+ start_mock_decoder(24);
+
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+
+ heif_error err = heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr);
+ REQUIRE(err.code == heif_error_Ok);
+
+ heif_track* track = heif_context_get_track(ctx, 0);
+ REQUIRE(track != nullptr);
+
+ for (int i = 0; i < 200; i++) {
+ heif_image* img = nullptr;
+ err = heif_track_decode_next_image(track, &img, heif_colorspace_undefined, heif_chroma_undefined, nullptr);
+ INFO("error: " << err.message << ", subcode: " << err.subcode << ", pushed: " << mock_num_pushed);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(img != nullptr);
+ heif_image_release(img);
+ }
+
+ heif_track_release(track);
+ heif_context_free(ctx);
+}
+
+
+TEST_CASE("one non-displayed sample per decoded frame stays within the work budget")
+{
+ std::vector<uint8_t> file = build_endless_visual_sequence(1);
+ start_mock_decoder(0, 2);
+
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+ REQUIRE(heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr).code == heif_error_Ok);
+ heif_track* track = heif_context_get_track(ctx, 0);
+ REQUIRE(track != nullptr);
+
+ for (int i = 0; i < 300; i++) {
+ heif_image* img = nullptr;
+ heif_error err = heif_track_decode_next_image(track, &img, heif_colorspace_undefined,
+ heif_chroma_undefined, nullptr);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(img != nullptr);
+ heif_image_release(img);
+ }
+
+ REQUIRE(mock_num_pushed == 600);
+ heif_track_release(track);
+ heif_context_free(ctx);
+}
+
+
+TEST_CASE("extra output frames do not create credit for future samples")
+{
+ std::vector<uint8_t> file = build_endless_visual_sequence(1);
+ start_mock_decoder(0, 1, 2);
+
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+ REQUIRE(heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr).code == heif_error_Ok);
+ heif_track* track = heif_context_get_track(ctx, 0);
+ REQUIRE(track != nullptr);
+
+ for (int i = 0; i < 200; i++) {
+ heif_image* img = nullptr;
+ heif_error err = heif_track_decode_next_image(track, &img, heif_colorspace_undefined,
+ heif_chroma_undefined, nullptr);
+ REQUIRE(err.code == heif_error_Ok);
+ REQUIRE(img != nullptr);
+ heif_image_release(img);
+ }
+
+ REQUIRE(mock_num_pushed == 100);
+ heif_track_release(track);
+ heif_context_free(ctx);
+}
+
+
+TEST_CASE("more than one non-displayed sample per frame eventually exhausts the work budget")
+{
+ std::vector<uint8_t> file = build_endless_visual_sequence(1);
+ start_mock_decoder(0, 3);
+
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+ REQUIRE(heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr).code == heif_error_Ok);
+ heif_track* track = heif_context_get_track(ctx, 0);
+ REQUIRE(track != nullptr);
+
+ heif_error err{};
+ heif_image* img = nullptr;
+ for (int i = 0; i < 300; i++) {
+ err = heif_track_decode_next_image(track, &img, heif_colorspace_undefined,
+ heif_chroma_undefined, nullptr);
+ if (err.code != heif_error_Ok) {
+ break;
+ }
+ REQUIRE(img != nullptr);
+ heif_image_release(img);
+ img = nullptr;
+ }
+
+ REQUIRE(err.code == heif_error_Invalid_input);
+ REQUIRE(err.subcode == heif_suberror_Security_limit_exceeded);
+ REQUIRE(img == nullptr);
+ REQUIRE(mock_num_pushed <= 2 * mock_num_output + 32);
+
+ int num_pushed_before_retry = mock_num_pushed;
+ err = heif_track_decode_next_image(track, &img, heif_colorspace_undefined,
+ heif_chroma_undefined, nullptr);
+ REQUIRE(err.code == heif_error_Invalid_input);
+ REQUIRE(mock_num_pushed == num_pushed_before_retry);
+
+ heif_track_release(track);
+ heif_context_free(ctx);
+}
+
+
+TEST_CASE("ignoring an edit list stops at the physical sample count")
+{
+ std::vector<uint8_t> file = build_endless_visual_sequence(3);
+ start_mock_decoder(no_output);
+
+ heif_context* ctx = heif_context_alloc();
+ REQUIRE(ctx != nullptr);
+ REQUIRE(heif_context_read_from_memory(ctx, file.data(), file.size(), nullptr).code == heif_error_Ok);
+ heif_track* track = heif_context_get_track(ctx, 0);
+ REQUIRE(track != nullptr);
+
+ heif_decoding_options* options = heif_decoding_options_alloc();
+ REQUIRE(options != nullptr);
+ options->ignore_sequence_editlist = 1;
+
+ heif_image* img = nullptr;
+ heif_error err = heif_track_decode_next_image(track, &img, heif_colorspace_undefined,
+ heif_chroma_undefined, options);
+ REQUIRE(err.code == heif_error_Decoder_plugin_error);
+ REQUIRE(img == nullptr);
+ REQUIRE(mock_num_pushed == 3);
+
+ heif_decoding_options_free(options);
+ heif_track_release(track);
+ heif_context_free(ctx);
+}