Commit f5f31760f8 for ffmpeg
commit f5f31760f81f15db09d25c84cfdd9c7d87ef12fd
Author: Jun Zhao <barryjzhao@tencent.com>
Date: Sat Oct 24 16:05:24 2026 +0800
tests/fate: cover chapter subframe alignment and non-synchsafe v2.4 CHAP
Extend fate-id3v2chap to verify that:
- unread trailing bytes after a NUL-terminated text subframe (TPE1) are
skipped so both TPE1 ("artist") and the following embedded subframe
(TIT2) are parsed;
- a non-conforming v2.4 CHAP frame with a plain 32-bit size whose high
bit is set (0x000000b6) is not truncated even when the stripped offset
contains a fitting empty frame header;
- a valid synchsafe v2.4 CHAP frame followed by an empty TPE1 frame and
a TALB frame preserves the subsequent frames;
- an ambiguous plain 256-byte v2.4 CHAP size (0x00000100, all high bits
clear) rejects the 128-byte synchsafe candidate whose fake header
exceeds the remaining tag length.
Signed-off-by: Jun Zhao <barryjzhao@tencent.com>
diff --git a/libavformat/tests/id3v2chap.c b/libavformat/tests/id3v2chap.c
index 760255bc17..536b219419 100644
--- a/libavformat/tests/id3v2chap.c
+++ b/libavformat/tests/id3v2chap.c
@@ -34,6 +34,7 @@ static int readback(const uint8_t *buf, int size)
FFIOContext read_pb;
ID3v2ExtraMeta *extra = NULL;
AVDictionary *meta = NULL;
+ const AVDictionaryEntry *album;
int ret = 1;
if (!in)
@@ -44,14 +45,22 @@ static int readback(const uint8_t *buf, int size)
if (ff_id3v2_parse_chapters(in, extra) < 0)
goto done;
- printf("chapters=%u\n", in->nb_chapters);
+ printf("chapters=%u", in->nb_chapters);
+ album = av_dict_get(meta, "album", NULL, 0);
+ if (album)
+ printf(" album=%s", album->value);
+ printf("\n");
for (unsigned i = 0; i < in->nb_chapters; i++) {
const AVChapter *ch = in->chapters[i];
- const AVDictionaryEntry *title = av_dict_get(ch->metadata, "title", NULL, 0);
+ const AVDictionaryEntry *title = av_dict_get(ch->metadata, "title", NULL, 0);
+ const AVDictionaryEntry *artist = av_dict_get(ch->metadata, "artist", NULL, 0);
- printf("chapter=%u time_base=%d/%d start=%"PRId64" end=%"PRId64" title=%s\n",
+ printf("chapter=%u time_base=%d/%d start=%"PRId64" end=%"PRId64" title=%s",
i, ch->time_base.num, ch->time_base.den, ch->start, ch->end,
title ? title->value : "(missing)");
+ if (artist)
+ printf(" artist=%s", artist->value);
+ printf("\n");
}
ret = 0;
@@ -64,17 +73,18 @@ done:
/* The first chapter title is long enough that its TIT2 frame is at least
* 128 bytes, so a plain size and a synchsafe size differ. */
-static int roundtrip(int version)
+static int roundtrip(int version, int ambiguous)
{
- char long_title[151];
+ char long_title[225];
+ int title_len = ambiguous ? 224 : 150;
AVFormatContext *out = NULL;
AVIOContext *pb = NULL;
ID3v2EncContext id3 = { 0 };
uint8_t *buf = NULL;
int size, chap_pos = -1, ret = 1;
- memset(long_title, 'L', sizeof(long_title) - 1);
- long_title[sizeof(long_title) - 1] = 0;
+ memset(long_title, 'L', title_len);
+ long_title[title_len] = 0;
out = avformat_alloc_context();
if (!out ||
@@ -86,7 +96,7 @@ static int roundtrip(int version)
ff_id3v2_start(&id3, pb, version, ID3v2_DEFAULT_MAGIC);
if (ff_id3v2_write_metadata(out, pb, &id3) < 0)
goto done;
- ff_id3v2_finish(&id3, pb, 10);
+ ff_id3v2_finish(&id3, pb, ambiguous ? 300 : 10);
size = avio_close_dyn_buf(pb, &buf);
pb = NULL;
if (size < ID3v2_HEADER_SIZE)
@@ -104,12 +114,82 @@ static int roundtrip(int version)
if (readback(buf, size))
goto done;
- /* Locate the first embedded TIT2 header inside the generated chapter.
- * Give it an oversized length, leaving the chapter and following CHAP
- * intact. The reader should retain both chapter timings. */
+ /* A 224-byte title makes the CHAP payload 256 bytes. First test a valid
+ * synchsafe 256-byte CHAP (00 00 02 00, raw 512) followed by an empty
+ * TPE1 frame and a TALB frame inside the 300-byte padding area, so
+ * check_tag() must accept nlen == 0 at the 256-byte boundary rather than
+ * falling through to the raw 512-byte offset in zero padding. Then test
+ * the non-conforming plain size 00 00 01 00, whose high bits are clear
+ * so the synchsafe candidate at 128 bytes must be rejected by checking
+ * the oversized LLLL frame header inside the title. */
+ if (ambiguous) {
+ if (chap_pos >= 0 && chap_pos + 10 + 256 + 45 + 27 <= size) {
+ uint8_t ch1_copy[45];
+ memcpy(ch1_copy, buf + chap_pos + 10 + 256, sizeof(ch1_copy));
+ memcpy(buf + chap_pos + 10 + 256, "TPE1\x00\x00\x00\x00\x00\x00", 10);
+ memcpy(buf + chap_pos + 10 + 266, "TALB\x00\x00\x00\x07\x00\x00\x03World", 17);
+ memcpy(buf + chap_pos + 10 + 283, ch1_copy, sizeof(ch1_copy));
+ printf("empty next frame version=4\n");
+ if (readback(buf, size))
+ goto done;
+ memcpy(buf + chap_pos + 10 + 256, ch1_copy, sizeof(ch1_copy));
+ memset(buf + chap_pos + 10 + 301, 0, 27);
+
+ buf[chap_pos + 6] = 0x01;
+ buf[chap_pos + 7] = 0x00;
+ printf("ambiguous plain CHAP version=4\n");
+ if (readback(buf, size))
+ goto done;
+ }
+ ret = 0;
+ goto done;
+ }
+
+ /* Non-conforming v2.4 CHAP with a plain 32-bit size (0x000000b6) whose
+ * high bit is set and whose payload contains uppercase ASCII at the
+ * stripped offset (0x36). Also zero the 4 size bytes after "LLLL" at
+ * offset 0x36 once to verify that is_syncsafe() rejects the 0x36
+ * candidate even when check_tag() would accept an empty frame header. */
+ if (version == 4 && chap_pos >= 0) {
+ uint8_t saved[4];
+ buf[chap_pos + 6] = 0x00;
+ buf[chap_pos + 7] = 0xb6;
+ printf("non-synchsafe CHAP version=4\n");
+ if (readback(buf, size))
+ goto done;
+
+ memcpy(saved, buf + chap_pos + 10 + 0x36 + 4, sizeof(saved));
+ memset(buf + chap_pos + 10 + 0x36 + 4, 0, sizeof(saved));
+ printf("high-bit plain CHAP with fitting fake header version=4\n");
+ if (readback(buf, size))
+ goto done;
+ memcpy(buf + chap_pos + 10 + 0x36 + 4, saved, sizeof(saved));
+ buf[chap_pos + 6] = 0x01;
+ buf[chap_pos + 7] = 0x36;
+ }
+
+ /* Split the 162-byte TIT2 subframe into a 22-byte TPE1 subframe with
+ * unread trailing bytes after its first NUL terminator, followed by a
+ * 140-byte TIT2 subframe, then test an oversized subframe length. */
if (chap_pos >= 0) {
- for (int i = chap_pos + 10; i + 10 <= size; i++) {
+ for (int i = chap_pos + 10; i + 33 <= size; i++) {
if (!memcmp(buf + i, "TIT2", 4)) {
+ uint8_t enc = buf[i + 10];
+ memcpy(buf + i, "TPE1", 4);
+ buf[i + 6] = 0;
+ buf[i + 7] = 12;
+ buf[i + 13] = 0;
+ memcpy(buf + i + 22, "TIT2\x00\x00", 6);
+ buf[i + 28] = version == 4 ? 0x01 : 0x00;
+ buf[i + 29] = version == 4 ? 0x02 : 0x82;
+ buf[i + 30] = 0;
+ buf[i + 31] = 0;
+ buf[i + 32] = enc;
+ printf("trailing text subframe version=%d\n", version);
+ if (readback(buf, size))
+ goto done;
+
+ memcpy(buf + i, "TIT2", 4);
memset(buf + i + 4, 0x7f, 4);
printf("invalid subframe version=%d\n", version);
if (readback(buf, size))
@@ -129,8 +209,9 @@ done:
int main(void)
{
- int ret = roundtrip(3);
+ int ret = roundtrip(3, 0);
- ret |= roundtrip(4);
+ ret |= roundtrip(4, 0);
+ ret |= roundtrip(4, 1);
return ret;
}
diff --git a/tests/ref/fate/id3v2chap b/tests/ref/fate/id3v2chap
index 512fbd3bd7..e711726f5f 100644
--- a/tests/ref/fate/id3v2chap
+++ b/tests/ref/fate/id3v2chap
@@ -3,6 +3,10 @@ CHAP size bytes=00 00 00 b6
chapters=2
chapter=0 time_base=1/1000 start=0 end=1000 title=LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
chapter=1 time_base=1/1000 start=1000 end=2000 title=End
+trailing text subframe version=3
+chapters=2
+chapter=0 time_base=1/1000 start=0 end=1000 title=LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL artist=LL
+chapter=1 time_base=1/1000 start=1000 end=2000 title=End
invalid subframe version=3
chapters=2
chapter=0 time_base=1/1000 start=0 end=1000 title=ch0
@@ -12,7 +16,32 @@ CHAP size bytes=00 00 01 36
chapters=2
chapter=0 time_base=1/1000 start=0 end=1000 title=LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
chapter=1 time_base=1/1000 start=1000 end=2000 title=End
+non-synchsafe CHAP version=4
+chapters=2
+chapter=0 time_base=1/1000 start=0 end=1000 title=LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
+chapter=1 time_base=1/1000 start=1000 end=2000 title=End
+high-bit plain CHAP with fitting fake header version=4
+chapters=2
+chapter=0 time_base=1/1000 start=0 end=1000 title=LLLLLLLLLLLLLLLLLLLLLLLLLLL
+chapter=1 time_base=1/1000 start=1000 end=2000 title=End
+trailing text subframe version=4
+chapters=2
+chapter=0 time_base=1/1000 start=0 end=1000 title=LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL artist=LL
+chapter=1 time_base=1/1000 start=1000 end=2000 title=End
invalid subframe version=4
chapters=2
chapter=0 time_base=1/1000 start=0 end=1000 title=ch0
chapter=1 time_base=1/1000 start=1000 end=2000 title=End
+version=4 declared=645 actual=645
+CHAP size bytes=00 00 02 00
+chapters=2
+chapter=0 time_base=1/1000 start=0 end=1000 title=LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
+chapter=1 time_base=1/1000 start=1000 end=2000 title=End
+empty next frame version=4
+chapters=2 album=World
+chapter=0 time_base=1/1000 start=0 end=1000 title=LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
+chapter=1 time_base=1/1000 start=1000 end=2000 title=End
+ambiguous plain CHAP version=4
+chapters=2
+chapter=0 time_base=1/1000 start=0 end=1000 title=LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
+chapter=1 time_base=1/1000 start=1000 end=2000 title=End