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