Summary
When TagLib-Sharp saves an MPEG-4 file (M4A/M4B) that contains a Nero-style chapter atom (moov/udta/chpl), it rewrites version 1 chpl atoms using version 0 format without adjusting the payload layout. This silently corrupts the chapter data and can cause other parsers that read chpl before ilst to fail to read any metadata from the file.
Affected Version
TagLibSharp-Lidarr 2.2.0.19 (fork of TagLib-Sharp). Believed to also affect upstream TagLib-Sharp — the chpl handling code has not been changed in the fork.
Steps to Reproduce
- Take any M4A or M4B file that contains a version 1 Nero chapter atom at
moov/udta/chpl.
- Open the file with
TagLib.File.Create(path).
- Modify any tag (e.g. set
tag.Title).
- Call
file.Save().
- Inspect the
chpl atom in the saved file.
Expected Behavior
The chpl atom should be preserved byte-for-byte (TagLib-Sharp does not read or expose chapter data, so it should pass the atom through unmodified), or at minimum be rewritten with a structurally valid format.
Actual Behavior
TagLib-Sharp rewrites the chpl atom with version byte 0x00 but retains the payload layout from version 1. The two versions have different structures:
Version 1 layout:
[4 bytes: atom size] [4 bytes: "chpl"] [1 byte: version=1] [3 bytes: flags]
[1 byte: reserved] ← version 1 has this extra byte
[4 bytes: chapter count (big-endian)]
[repeated: 8-byte timestamp + 1-byte name length + name bytes]
Version 0 layout:
[4 bytes: atom size] [4 bytes: "chpl"] [1 byte: version=0] [3 bytes: flags]
← no reserved byte
[4 bytes: chapter count (big-endian)]
[repeated: 8-byte timestamp + 1-byte name length + name bytes]
After file.Save(), the atom is written as version 0 but the payload still includes the version 1 reserved byte. This shifts the chapter count and all subsequent chapter data by 1 byte. The result is:
- The chapter count field reads garbage (the reserved byte
0x00 becomes the high byte of the count).
- All chapter timestamps and names are shifted and unreadable.
- Parsers that strictly validate the
chpl atom may reject it entirely.
Downstream Impact
Parsers that process udta children sequentially and encounter the corrupted chpl before reaching ilst (the iTunes metadata atom) may abort parsing. This has been observed with:
- ATL (z440.atl.core) — the audio metadata library used by Jellyfin
- Jellyfin — fails to read any metadata from files with corrupted
chpl atoms
This means a simple tag write by TagLib-Sharp can render a file's metadata completely unreadable to Jellyfin and other applications, even though TagLib-Sharp itself can still read the file (because it reads ilst directly by seeking to it).
Workaround
We currently post-process every MPEG-4 file after file.Save() by locating the chpl atom at moov/udta/chpl and replacing the 4-byte type field with "free" to neutralize it:
// After file.Save(), if TagTypes includes Apple:
using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite))
{
var chplOffset = FindNestedAtom(fs, 0, fs.Length, "moov", "udta", "chpl");
if (chplOffset < 0) return;
// Validate: read version byte
fs.Seek(chplOffset + 8, SeekOrigin.Begin);
var version = fs.ReadByte();
if (version == 0)
{
// Version 0 written by TagLib-Sharp — likely corrupted from version 1
// Neutralize by replacing atom type with "free"
fs.Seek(chplOffset + 4, SeekOrigin.Begin);
fs.Write(new byte[] { 0x66, 0x72, 0x65, 0x65 }, 0, 4); // "free"
}
}
This destroys chapter data but preserves file structure and unblocks downstream parsers. It's not a proper fix — chapter information is lost.
Suggested Fix
One of:
- Preserve
chpl verbatim — Since TagLib-Sharp doesn't expose chapter data, the chpl atom should be passed through unchanged during save. This is the simplest and safest fix.
- Respect version on rewrite — If the atom must be rewritten (e.g., due to offset changes), preserve the original version byte and write the payload in the corresponding format.
- Omit
chpl on rewrite — If correct rewriting is too complex, drop the atom rather than writing a corrupt one. Losing chapters is better than breaking all metadata readers.
Environment
Summary
When TagLib-Sharp saves an MPEG-4 file (M4A/M4B) that contains a Nero-style chapter atom (
moov/udta/chpl), it rewrites version 1chplatoms using version 0 format without adjusting the payload layout. This silently corrupts the chapter data and can cause other parsers that readchplbeforeilstto fail to read any metadata from the file.Affected Version
TagLibSharp-Lidarr 2.2.0.19(fork of TagLib-Sharp). Believed to also affect upstream TagLib-Sharp — thechplhandling code has not been changed in the fork.Steps to Reproduce
moov/udta/chpl.TagLib.File.Create(path).tag.Title).file.Save().chplatom in the saved file.Expected Behavior
The
chplatom should be preserved byte-for-byte (TagLib-Sharp does not read or expose chapter data, so it should pass the atom through unmodified), or at minimum be rewritten with a structurally valid format.Actual Behavior
TagLib-Sharp rewrites the
chplatom with version byte0x00but retains the payload layout from version 1. The two versions have different structures:Version 1 layout:
Version 0 layout:
After
file.Save(), the atom is written as version 0 but the payload still includes the version 1 reserved byte. This shifts the chapter count and all subsequent chapter data by 1 byte. The result is:0x00becomes the high byte of the count).chplatom may reject it entirely.Downstream Impact
Parsers that process
udtachildren sequentially and encounter the corruptedchplbefore reachingilst(the iTunes metadata atom) may abort parsing. This has been observed with:chplatomsThis means a simple tag write by TagLib-Sharp can render a file's metadata completely unreadable to Jellyfin and other applications, even though TagLib-Sharp itself can still read the file (because it reads
ilstdirectly by seeking to it).Workaround
We currently post-process every MPEG-4 file after
file.Save()by locating thechplatom atmoov/udta/chpland replacing the 4-byte type field with"free"to neutralize it:This destroys chapter data but preserves file structure and unblocks downstream parsers. It's not a proper fix — chapter information is lost.
Suggested Fix
One of:
chplverbatim — Since TagLib-Sharp doesn't expose chapter data, thechplatom should be passed through unchanged during save. This is the simplest and safest fix.chplon rewrite — If correct rewriting is too complex, drop the atom rather than writing a corrupt one. Losing chapters is better than breaking all metadata readers.Environment
TagLibSharp-Lidarr 2.2.0.19(also likely affects upstream TagLib-Sharp)chplchapters — common in audiobooks)=Workaround example:[bookshelf][https://github.com/persist credits; new audio tags; sanitize tags; docker dev pennydreadful/bookshelf#140](feature fork)