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>
This commit is contained in:
Jun Zhao authored and Jun Zhao committed 2026-10-24 15:15:50 +00:00
1 parent 260d32d06d
commit f5f31760f8
2 files changed
+124 -14

No files matched your search

+95 -14
View File
@@ -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;
}
+29
View File
@@ -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