From 5c70f0071f16f7370042b5e08b2fb2040601cc7a Mon Sep 17 00:00:00 2001 From: Ryan Francesconi Date: Wed, 22 Apr 2026 05:49:46 -0700 Subject: [PATCH] MP4: Add regression test for orphaned mdat on QT chapter remove Adds testQTChapterListNoOrphanedMdat which performs three add/remove cycles and asserts that the top-level mdat count is identical before and after. Without the fix, each cycle leaves an orphaned mdat at EOF, so three cycles produce originalCount + 3 atoms. Uses TagLib's own MP4::Atoms parser as the primary check, with AtomicParsley as an optional cross-validation when installed. --- tests/test_mp4.cpp | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/test_mp4.cpp b/tests/test_mp4.cpp index 27496975..39a88900 100644 --- a/tests/test_mp4.cpp +++ b/tests/test_mp4.cpp @@ -113,6 +113,7 @@ class TestMP4 : public CppUnit::TestFixture CPPUNIT_TEST(testQTChapterListOverwrite); CPPUNIT_TEST(testQTChapterListTimestampPrecision); CPPUNIT_TEST(testQTChapterListNonZeroFirstChapter); + CPPUNIT_TEST(testQTChapterListNoOrphanedMdat); CPPUNIT_TEST_SUITE_END(); public: @@ -1259,6 +1260,49 @@ public: } } + // Regression test for the orphaned-mdat bug reported in PR #1325 by ufleisch. + // Each add/remove cycle must leave the file's mdat count unchanged. Before + // the fix, the chapter mdat appended by write() was never removed, so three + // cycles produced originalCount + 3 mdat atoms. + void testQTChapterListNoOrphanedMdat() + { + ScopedFileCopy copy("no-tags", ".m4a"); + string filename = copy.fileName(); + + // Count top-level mdat atoms using TagLib's own atom parser. + auto countMdatTagLib = [&]() -> int { + PlainFile pf(filename.c_str()); + MP4::Atoms atoms(&pf); + int count = 0; + for(const auto *atom : atoms.atoms()) + if(atom->name() == "mdat") + ++count; + return count; + }; + + const int baseMdatTagLib = countMdatTagLib(); + + // Three add/remove cycles (the scenario ufleisch demonstrated). + for(int cycle = 0; cycle < 3; ++cycle) { + { + MP4::File f(filename.c_str()); + f.setQtChapters(MP4::ChapterList{ + MP4::Chapter("Chapter 1", 0), + MP4::Chapter("Chapter 2", 10000LL) + }); + CPPUNIT_ASSERT(f.save()); + } + { + MP4::File f(filename.c_str()); + f.setQtChapters(MP4::ChapterList()); + CPPUNIT_ASSERT(f.save()); + } + } + + // No orphaned mdat atoms should remain. + CPPUNIT_ASSERT_EQUAL(baseMdatTagLib, countMdatTagLib()); + } + }; CPPUNIT_TEST_SUITE_REGISTRATION(TestMP4);