From 747c6ca12653f6c0d9aadfd81645c3b008d5dea5 Mon Sep 17 00:00:00 2001 From: Ryan Francesconi <2917795+ryanfrancesconi@users.noreply.github.com> Date: Sat, 31 Oct 2026 05:42:00 -0700 Subject: [PATCH] ID3v2: Render at most 255 child elements in a CTOC frame (#1479) The entry count is a single byte, so a table of more than 255 children wrapped the count while still writing every child element ID, and readers took the surplus IDs as embedded frames. Render the first 255 with a matching count, as renderChplData does for Nero chapters. --- .../id3v2/frames/tableofcontentsframe.cpp | 8 ++++++- tests/test_id3v2.cpp | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/taglib/mpeg/id3v2/frames/tableofcontentsframe.cpp b/taglib/mpeg/id3v2/frames/tableofcontentsframe.cpp index cac8f7f3..d4767393 100644 --- a/taglib/mpeg/id3v2/frames/tableofcontentsframe.cpp +++ b/taglib/mpeg/id3v2/frames/tableofcontentsframe.cpp @@ -25,6 +25,7 @@ #include "tableofcontentsframe.h" +#include #include #include "tpropertymap.h" @@ -293,8 +294,13 @@ ByteVector TableOfContentsFrame::renderFields() const if(d->isOrdered) flags += 1; data.append(flags); - data.append(static_cast(entryCount())); + // The entry count is a single byte, so only the first 255 child elements are rendered. + const unsigned int count = std::min(entryCount(), 255U); + data.append(static_cast(count)); + unsigned int i = 0; for(const auto &element : std::as_const(d->childElements)) { + if(i++ >= count) + break; data.append(element); data.append('\0'); } diff --git a/tests/test_id3v2.cpp b/tests/test_id3v2.cpp index 502a755e..ba2127a4 100644 --- a/tests/test_id3v2.cpp +++ b/tests/test_id3v2.cpp @@ -137,6 +137,7 @@ class TestID3v2 : public CppUnit::TestFixture CPPUNIT_TEST(testRenderChapterFrame); CPPUNIT_TEST(testParseTableOfContentsFrame); CPPUNIT_TEST(testRenderTableOfContentsFrame); + CPPUNIT_TEST(testRenderTableOfContentsFrameWithManyChildren); CPPUNIT_TEST(testShrinkPadding); CPPUNIT_TEST(testEmptyFrame); CPPUNIT_TEST(testDuplicateTags); @@ -1707,6 +1708,28 @@ public: f.render()); } + void testRenderTableOfContentsFrameWithManyChildren() + { + const unsigned int counts[] = { 255, 256, 300 }; + for(unsigned int count : counts) { + ID3v2::Header header; + ID3v2::TableOfContentsFrame f(&header, "CTOC"); + f.setElementID("T"); + for(unsigned int i = 0; i < count; ++i) + f.addChildElement(ByteVector("C") + ByteVector(String::number(i).toCString())); + auto eF = new ID3v2::TextIdentificationFrame("TIT2"); + eF->setText("TC1"); + f.addEmbeddedFrame(eF); + + ID3v2::TableOfContentsFrame parsed(&header, f.render()); + CPPUNIT_ASSERT_EQUAL(255U, parsed.entryCount()); + CPPUNIT_ASSERT_EQUAL(ByteVector("C254"), parsed.childElements().back()); + CPPUNIT_ASSERT_EQUAL(1U, parsed.embeddedFrameList().size()); + CPPUNIT_ASSERT_EQUAL(ByteVector("TIT2"), + parsed.embeddedFrameList().front()->frameID()); + } + } + void testShrinkPadding() { ScopedFileCopy copy("xing", ".mp3");