Commit Graph
2790 Commits
Author SHA1 Message Date
MinnieTheMoocherandGitHub ba4eba9d41 AIFF: Support historic NAME/AUTH/(c)/ANNO text chunks (#1488)
(defaulting to Mac OS Roman charset (Windows codepage 10000),
overridable by user)

Before this commit, only ID3 tags were supported, but AIFF also historically
features 4 text chunks, see
https://web.archive.org/web/20180613135101/https://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/AIFF/Docs/AIFF-1.3.pdf,
chapter "Text Chunks - Name, Author, Copyright, Annotation".
2026-11-03 19:56:12 +01:00
Ryan FrancesconiandGitHub 52d0f7cc5e FileRef: Rewind the stream before detecting the type by content (#1487)
A parser tried for the file extension may leave the stream anywhere, and Matroska::File::read() starts at the current position, so a Matroska file under an extension whose parser rejects it gave a null FileRef.
2026-11-01 07:40:57 +01:00
Ryan FrancesconiandGitHub 8875f397d9 MPEG: Scan raw bytes in 64 KB blocks when searching for frames (#842)
findID3v2, nextFrameOffset and previousFrameOffset read raw bytes in
64 KB blocks instead of building a ByteVector per byte from 1 KB reads,
with the sync check in an isFrameSync(b1, b2) overload. Results are
unchanged; junk before or after the audio no longer makes opening slow.
2026-11-01 07:26:20 +01:00
Ryan FrancesconiandGitHub d1bd220c26 Matroska: Read SamplingFrequency and Channels defaults when absent (#1358)
An Audio element without SamplingFrequency or Channels now reads their
defaults (RFC 9559: 8000 Hz, one channel) instead of 0, and the track
is no longer skipped when BitDepth is absent too. A track without an
Audio element is still skipped.
2026-11-01 07:06:07 +01:00
Ryan FrancesconiandGitHub 629b707fc5 ByteVector: Skip whitespace in fromBase64 (#1484)
ASCII whitespace (space, tab, CR, LF) is skipped before decoding, so
line-wrapped base64, such as a METADATA_BLOCK_PICTURE wrapped every 76
characters, decodes instead of returning an empty vector. Other
invalid characters still do.
2026-11-01 06:57:14 +01:00
Ryan FrancesconiandGitHub 40503ebd07 ID3v2: Skip an unreadable frame instead of ending the tag (#1483)
A frame that cannot be read, such as one with an invalid ID, is now
skipped when another frame, padding or the end of the frames follows
where its size ends; anything else still ends the tag as before.
Previously every frame after it was dropped, and a save then deleted
them from the file.
2026-11-01 06:53:15 +01:00
Ryan FrancesconiandGitHub 96c7893332 RIFF: Don't let a clamped chunk swallow chunks saved after it (#1482)
Only the audio chunk (data, or SSND in AIFF) is clamped to the end of
the file when it declares more than the file holds, and its size is
rewritten before a chunk is appended after it. Any other chunk running
past the end, such as a trailing APEv2 tag, ends the chunk walk again,
so a save inserts new chunks ahead of it instead of losing them on the
next read.
2026-11-01 06:39:07 +01:00
Ryan FrancesconiandGitHub 80e42828a8 ID3v2: Update an edited frame in place in setProperties (#1481)
When exactly one frame holds an edited key and its type can hold the
new value, setProperties() now updates that frame instead of deleting
it and appending a new one. A COMM or USLT keeps its language, a TXXX
its description's spelling, and each keeps its position and its stored
text encoding, which is promoted at render when the new text needs it.
2026-11-01 06:29:52 +01:00
Ryan FrancesconiandGitHub 3bce7df062 RIFF: Read INFO text that isn't valid UTF-8 as Latin-1 when UTF-8 is assumed (#1480)
When setAssumedCodePage(65001) supplies the charset for an INFO tag
whose file declares none, a field whose bytes are not valid UTF-8 is
now read as ISO 8859-1, the RIFF default, instead of as an empty
string. A code page the file declares is never second-guessed, and a
handler installed with setStringHandler() still takes precedence.
2026-11-01 05:49:03 +01:00
Ryan FrancesconiandGitHub 747c6ca126 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.
2026-10-31 13:42:00 +01:00
Ryan FrancesconiandGitHub 36c73e883b RIFF/WAV: Add DISP chunk support (#1478)
Give raw access to the DISP chunks of a WAV file, in the way the bext
and iXML chunks already are: DISPData(), setDISPData() and
hasDISPData(). A file can hold several DISP chunks, one per clipboard
format, so they are read and written as a list in file order. The
DISP chunks are only rewritten on save when setDISPData() was called.

RIFF::File gains a protected appendChunk(), which creates a chunk even
when one with the same name exists; setChunkData() now uses it.
2026-10-31 13:41:39 +01:00
Ryan FrancesconiandGitHub 4accfdafae Toolkit: Add DeferredWriteStream, which moves displaced bytes once (#1476)
An IOStream over another stream that records writes, insertions,
removals and truncations in memory and applies them in one commit(),
moving each displaced byte at most once. bytesToMove() reports what
commit() would move, and writeTo() writes the result to another stream
instead. Opt-in; no File subclass changes.

See #1474.
2026-10-31 13:40:54 +01:00
Nathan ChandrasekarandGitHub 50ca520b27 ByteVector, List: Check indices if the standard library does (#1467)
If the standard library checks the index in std::vector::operator[]
(_GLIBCXX_ASSERTIONS, libc++ hardening modes), ByteVector::operator[]
and List::operator[] now check the index against their own size and
abort if it is out of range, as std::vector does.

The std::vector check alone is not enough: a ByteVector created by
mid() shares the buffer of the original byte vector, so an index past
the end of the smaller vector still passes it. List::operator[] walks
a std::list, which these checks do not cover.

Builds without these macros are unchanged; a GCC Release build of
libtag produces identical code. The Ubuntu CI job now builds with
-D_GLIBCXX_ASSERTIONS so that the tests run with the checks enabled.
2026-10-31 12:07:55 +01:00
Urs Fleisch cfb9a7d8a9 test_matroska.cpp: Fix -Wconversion warnings 2026-10-31 11:00:15 +01:00
MinnieTheMoocherandUrs Fleisch 66a523d685 Charset: Latin1: when encoding, substitute '?' instead of truncating
String::data(Latin1) keeps only the low byte of each code point, so the
fast path wrote garbage for characters ISO 8859-1 cannot hold,
instead of '?' as documented in tcharset.h:
     * When encoding into a legacy code page, characters which cannot be
     * represented by the code page are replaced by '?'.
2026-10-31 10:58:01 +01:00
ff3b53670a RIFF/WAV: Default to latin1, change charset when CSET chunk is present (#1451)
- WAV files being read evaluate the charset properly (overridable
  via option by caller)
- WAV files being saved always use UTF-8 (they add a CSET chunk)

following the specification at
https://www.robotplanet.dk/audio/wav_meta_data/riff_mci.pdf#page=25

The code page charset can also be used for ID3v1 tags, both for
parsing and rendering.

For users in space restricted environments, there
is a build flag WITH_CHARSETTABLES to exclude the character tables.

---------

Co-authored-by: Urs Fleisch <[email protected]>
2026-10-28 17:49:28 +01:00
Ryan FrancesconiandGitHub bdfeb82cbe MP4: Cover the leading QuickTime placeholder chapter with an empty edit (#1473)
QuickTime chapter samples run back to back from media time 0, so when the
first chapter starts after 0, QtChapterList::write() prepends an untitled
placeholder sample to keep the absolute start times.  TagLib's own reader
strips it again, but the chapter track's edit list played the whole media
from time 0, so AVFoundation and ffmpeg listed the placeholder as an
untitled first chapter.

The edit list now begins with an empty edit spanning the placeholder,
followed by the media from the first written chapter.  The placeholder
sample is kept, so readers which ignore edit lists see the same chapters
as before.  A list starting at 0 still gets a single edit.

testQTChapterListNonZeroFirstChapterEditList writes chapters at 1 s and
2 s and checks the chapter track's elst: an empty edit (media time -1) of
one second in the mvhd timescale, then the remaining duration from media
time one second.  It also checks that a list starting at 0 gets a single
edit from media time 0, and that qtChapters() reads back exactly the
written chapters in both cases.
2026-10-28 17:37:46 +01:00
Ryan FrancesconiandGitHub 53a80ba166 ID3v2: Keep frames whose combined values are unchanged in setProperties (#1472)
properties() merges the values of all frames mapping to the same key, in
frame order, so a key can be spread over several frames, e.g. two
comments without a description in different languages give
COMMENT=["hello", "hallo"]. setProperties() only kept a frame if its own
values equal the whole requested list, so none of these frames was kept,
and the list was then written as a single TXXX:COMMENT frame. This made
setProperties(properties()) lossy: the COMM frames and their languages
were replaced by a frame no comment reader looks for.

Before deciding which frames to delete, merge the values of the frames
holding a single key in frameList() order, as properties() does. If the
requested values for a key equal the merged ones, keep all of its frames
and do not create a new frame for it. TIPL and TMCL frames, and every key
whose values changed, are handled as before.

testSetPropertiesKeepsSpreadFrames covers the identity round trip on two
COMM frames, a request matching only one of them (kept as before), a
changed list (still a single TXXX), a key held by a TXXX and a COMM
frame (compared in frame order, not frame ID order), and a save and
reread of an MPEG file.
2026-10-28 17:29:16 +01:00
Ryan FrancesconiandGitHub 9d09f9b413 RIFF: Report unknown INFO fields by their ID in properties() (#1471)
RIFF::Info::Tag::properties() passed the result of the failed property
key lookup, an empty string, to addUnsupportedData() instead of the
field ID. An INFO chunk with a field that has no property mapping
therefore reported an empty entry in unsupportedData(), and passing that
list to removeUnsupportedProperties(), which erases by ID, removed
nothing.

Report the field ID instead, decoded as Latin-1 to match how
removeUnsupportedProperties() encodes it.

The new test sets a mapped field (INAM) and an unmapped one (ITRK),
checks that unsupportedData() is ["ITRK"], and that
removeUnsupportedProperties() with that list removes ITRK and keeps
INAM.
2026-10-28 17:12:02 +01:00
Ryan FrancesconiandGitHub 1ae164e382 Matroska: Place new elements correctly when AvoidInsert moves an element (#1470)
When a save with WriteStyle::AvoidInsert moved an existing element to the
end of the segment and also added a new element, the new element was
written past the end of the file. New elements are given the old end of
the file as their offset before rendering, and that offset is then shifted
by the size change of each moved element, while the pass which lays out
the moved elements after rendering did not include the new ones.

This happens, for example, when the tags before the first cluster grow
beyond their slot and an attachment is added in the same save. The file
is left with zero bytes between the last element and the new one, the
segment size does not cover the new element, and the seek head points to
the wrong position, so the attachment cannot be read back.

The new elements are now laid out together with the moved elements,
contiguously up to the end of the segment, and their seek head entries are
updated as well.

testSaveTypesMoveAndAdd grows the tags of tags-before-cues.mkv and adds
an attachment in one AvoidInsert save, then checks that the cluster did
not move, that the children of the segment run contiguously to its
declared end, which is the end of the file, and that the tag and the
attachment read back.
2026-10-27 05:05:35 +01:00
Ryan FrancesconiandGitHub ab3526af2e Matroska: Render element sizes at a size length limit with a valid length (#1469)
EBML::minSize() chose the size length for an element's data size with
<= at each limit, so a size of exactly 2^(7n) - 1 (127, 16383, 2097151,
...) was rendered in n bytes with every value bit set. That is the
reserved "unknown size" value (RFC 8794 section 6.2), so a reader no
longer knows where the element ends. A tag value of exactly 127 bytes,
for example, saves without error but reads back as an empty string.

minSize() also returned 0 for any size from 0x7FFFFFFFF (32 GiB) up, so
renderVINT() wrote a size of zero bytes. Sizes up to 2^56 - 2 now use
size lengths 6 to 8; larger sizes still return 0, as no size length can
hold them.

testElementSizeLength checks minSize() on either side of the limit of
each size length from 1 to 8 bytes. testSaveElementSizeAtLengthLimit
saves titles of 126 to 128 and 16382 to 16384 bytes to no-tags.mka and
reads them back.
2026-10-27 04:59:21 +01:00
Urs FleischandGitHub 5fb109989d Check if libatomic is needed for 64-bit atomic operations (#1465) 2026-10-19 17:37:18 +02:00
Nathan ChandrasekarandGitHub 55ceae49ba ASF: Return excess values from setProperties (#1464) 2026-10-17 08:36:56 +02:00
96970251ce MP4: Reuse parsed atoms when lazily reading chapters (#1460)
neroChapters() and qtChapters() parsed the whole atom tree again on first
use, although the file already holds it from opening. They now use the
parsed atoms, and parse them again after a save or strip, which can move
atoms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 07:32:51 +02:00
Acts1631andGitHub 649607f1fc Limit Matroska EBML element allocations (#1456)
MasterElement permits up to one million elements per read. Segment
metadata sections each start a fresh counter, allowing small files
with empty elements to exhaust memory during parsing.

Share one count across segment sections and seek-head lookups. Bound
the count by file size, with a minimum for small valid files.
2026-10-16 15:32:51 +02:00
MinnieTheMoocherandGitHub 2323c563e2 ASF: Support rating tags in asf/wma files (#1447) 2026-10-10 09:08:45 +02:00
FelipeandGitHub 6c01a5f197 docs: Fix and complete the property mapping documentation
* docs: Reword property mapping notes

Use the present tense, refer to keys instead of tags, fix the comma
splice in the Matroska note, and describe the Matroska target level as
optional, with /30 (Track) used when none is given.

* docs: Fix ID3v2 TIPL and TMCL property mappings

ARRANGER, ENGINEER, PRODUCER, DJMIXER and MIXER are stored as roles in the
TIPL frame, and TMCL instrument/name pairs map to PERFORMER:<instrument>
keys. The INVOLVEDPEOPLE and MUSICIANCREDITS keys listed for these frames
don't exist in the code: their frameTranslation entries are commented out,
so both are written to TXXX like any unlisted key.

* docs: Widen ID3v2 column of property mapping table

Whitespace only, to make room for the TXXX descriptions added in the next
commit.

* docs: Document missing ID3v2 property mappings

- MUSICBRAINZ_TRACKID is stored in a UFID frame with owner
  http://musicbrainz.org.
- MusicBrainz, AcoustID and MusicIP properties use fixed TXXX
  descriptions from txxxFrameTranslation (e.g. MUSICBRAINZ_ALBUMID as
  "MUSICBRAINZ ALBUM ID"), not the key.
- COMMENT, LYRICS and URL keys with a description suffix are stored in
  COMM, USLT and WXXX frames with that description.

* docs: Describe MP4 freeform fallback and remove derivable names

MP4 keys without a listed name are stored in a freeform atom
(----:com.apple.iTunes: followed by the key), so cells repeating exactly
that name are redundant. The now empty LICENSE row is kept to list the
key.

* docs: Document MP4 rate atom for RATING

The code maps RATING to the MP4 rate atom, but it was missing from the
table entirely.

* docs: Fix Matroska target levels

The code maps RELEASEDATE to DATE_RELEASED at Album level (50), not Track
level (30). DATE_RECORDED for DATE uses the default level (30) and, unlike
TRACKNUMBER or REPLAYGAIN_TRACK_GAIN, has no Album-level counterpart to
tell apart, so its level is omitted.

* docs: Document Matroska REPLAYGAIN property mappings

The code maps ReplayGain properties to Matroska tags, but these were missing from the table entirely.

* docs: Note that RIFF INFO and ASF only support listed keys

Unlike the other formats, RIFF INFO and ASF don't store keys without a
listed name, and setProperties() returns them.

* docs: Fix ASF mapping for ASIN

The ASF attribute "ASIN" was incorrectly listed under AUDIOSOURCEWEBPAGE.

* docs: Document missing ASF attribute mappings

The code maps the ReplayGain properties to replaygain_* attributes and
further Windows Media attributes (e.g. WM/ToolName, IsVBR,
WM/MediaClassPrimaryID) to properties, but they were missing from the
table.

* docs: Document ASF content description fields

TITLE, ARTIST, COPYRIGHT and COMMENT are stored in the Content Description
Object (Title, Author, Copyright, Description) rather than as attributes,
so the empty ASF column made them look unsupported.

* docs: Remove stray ul tag from TextIdentificationFrame table
2026-10-10 09:06:17 +02:00
Urs FleischandGitHub 961dd6936d Merge pull request #1453 from ufleisch/wav-32-bit
RIFF: avoid 32-bit offset_t overflow in chunk size clamping (#1446)
2026-10-02 17:02:10 +02:00
MinnieTheMoocherandGitHub 9174df7bf6 MP4: Use rate atom for RATING property (#1444) 2026-09-30 18:57:11 +02:00
Acts1631andGitHub 5bd11b2fb6 Bound text value counts in ID3v2 and APE tags (#1458)
Null-separated text values create a ByteVector and String for each
tiny field. A 1 MB tag can allocate over 100 MB and abort
memory-limited applications.

Limit decoded values based on input size and stop before creating
String objects when the value count exceeds the budget.
2026-09-28 20:28:51 +02:00
Acts1631andGitHub 62db51159d Avoid repeated genre suffix copies in ID3v2 parsing (#1457)
A TCON frame with many legacy genre codes copies the remaining text
for every code. A small crafted tag can consume seconds of CPU.

Walk the original string by offset, compare mapped genres without
copying the suffix, and limit decoded codes by input size.
2026-09-28 20:28:19 +02:00
MinnieTheMoocherandGitHub f9efbc7ba8 RIFF: Do not create INFO fields from a truncated chunk header (#1454) 2026-09-27 16:56:14 +02:00
Boyuan YangandUrs Fleisch 7b54383b84 RIFF: avoid 32-bit offset_t overflow in chunk size clamping (#1446)
TagLib::offset_t is off_t on POSIX, which is a 32-bit signed type on
32-bit platforms built without _FILE_OFFSET_BITS=64 (e.g. Debian i386).
The RF64 support introduced in 2.3.2 clamps chunk sizes with
std::min<offset_t>(size, 0xffffffff): with a 32-bit offset_t the constant
converts to -1 and becomes the result, so chunkDataSize() returns
4294967295 and the WAV/AIFF/RIFF parsers misbehave. In read(), a declared
size above 2 GiB likewise turns negative before the clamping check.

Perform the clamping in 64-bit types instead. Fixes the test suite
failures (TestRIFF, TestWAV, TestAIFF) observed on Debian i386. The same
clamp in WAV::Properties is fixed as well.
2026-09-27 12:16:37 +02:00
Urs Fleisch 1b56c77c67 Fix sign-compare warnings when building for 32-bit 2026-09-27 12:16:37 +02:00
Urs Fleisch cf45bce73c GitHub Actions: Build with Ubuntu 32-bit 2026-09-27 12:16:37 +02:00
GarryHandGitHub 8503099543 Matroska: Raise the global EBML element bound above real-file needs
[5f869755] gave the whole-file element budget the same value as the per-container
limit (50000). A real 6,027,872,050-byte .mka requires 99780 elements, so
MasterElement::read() returns false and the file is unopenable at every ReadStyle.

Raise the global bound to 1000000, leaving the per-level bound untouched. That is
~10x a large real file's requirement and still an order of magnitude below the
'millions of small elements' case the original commit guards against.
2026-09-27 08:21:09 +02:00
Urs FleischandGitHub 0faf7291ca ASF: Verify if header size is valid
When saving a .wma file, ASF::File::save() does not validate the
header's headerSize field before using it in arithmetic. If its value
is below 30, an unsigned underflow and a subsequent integer overflow
cause FileStream::removeBlock() to compute a corrupted target offset,
resulting in an unbounded copy loop.

Credits for the discovery of this bug go to Ximena Molina Portilla,
Software Engineering student at Instituto Tecnológico de Costa Rica
(GitHub: @ximemolina).
2026-09-11 19:32:08 +02:00
Stephen BoothandGitHub 4265461e9b Skip unknown ASF values (#1439) 2026-09-09 20:42:30 +02:00
Stephen BoothandGitHub 2c3d1e14ff Improve string to ASF BoolType conversion (#1435) 2026-09-09 20:36:33 +02:00
Urs Fleisch deadc29907 Version 2.3.2 v2.3.2 2026-09-05 06:17:23 +02:00
roshanandGitHub d6c5a8d888 Add fuzz target for FileRef parsing under tests/fuzzing (#1433)
Adds a libFuzzer target parsing arbitrary input through TagLib::FileRef,
a format magic dictionary, and a README with build and run instructions.
Fully decoupled from the normal build as discussed in #1431.
2026-08-30 13:07:57 +02:00
Acts1631andGitHub 5fa6fa6c2f IT: reject invalid files before saving (#1432) 2026-08-29 07:45:08 +02:00
Urs Fleisch 6dd7c9f313 Prepare 2.3.2 release 2026-08-27 17:39:35 +02:00
Urs FleischandGitHub 3672fa6dd2 ASF: Skip attributes with invalid data type (#1429) 2026-08-27 17:35:54 +02:00
Acts1631andGitHub d391029695 Resolve QuickTime chunk tables linearly (#1428)
A QuickTime chapter track can provide large stco and stsc
tables. QtChapterList::resolveSampleOffsets() currently walks every stsc
entry for every chunk, so a file with N entries can force O(N²) work during
MP4::File::qtChapters(). A 3.2 MB file with 200,000 entries took about 20
seconds in the release build.

Keep a cursor into stscEntries and advance it as chunk numbers
increase. This resolves the same ordered table in linear time while
avoiding attacker-controlled repeated scans.
2026-08-21 05:59:27 +02:00
Acts1631andGitHub 0a3deeac75 Reject QuickTime chapter samples outside their data bounds (#1427)
A malformed QuickTime chapter track can set stsz's default sample size to a
value larger than the available sample data. MP4::File::qtChapters() passes
that value through QtChapterList::read() to readTextSample(), allowing a
small file to cause gigabytes of allocations and terminate the application.

Check each resolved sample against the file end and the next sample offset
before reading it. Samples that do not fit are rejected without allocating
the attacker-controlled size.
2026-08-21 05:43:54 +02:00
Acts1631andGitHub 5f869755c8 Bound recursive Matroska and MP4 element trees (#1425)
The Matroska and MP4 parsers limited child elements per container, but
allowed that limit to repeat at every nesting level. A crafted media file
could therefore retain millions of small elements and exhaust process
memory while it was opened.

Carry a shared element count through recursive parsing so the existing
per-container limits also have a global bound.
2026-08-19 18:25:21 +02:00
Acts1631andGitHub 94f7b5af4c XM: assume 40 bytes for zero sample header size (#1422)
A zero sample header size makes the sample loop consume no input,
allowing crafted instrument and sample counts to cause excessive CPU
and memory use during parsing.

Some trackers write zero sample header sizes, some trackers ignore
such values and assume a standard header size of 40 bytes. We do the
same to ensure the sample loop advances.
2026-08-19 05:36:58 +02:00
Acts1631andGitHub fa4c623ab1 Bound ASF header-extension child parsing (#1426)
ASF header extensions trusted their nested data size and accepted
zero-sized child objects. A crafted file could make parsing continue
outside the enclosing object while retaining one object per child
header, exhausting process memory when opened.

Bound nested data to the enclosing object, require a complete child
header, and cap the nested object count.
2026-08-18 21:27:26 +02:00
Acts1631andGitHub 0a13a11bad Xiph: bound comment field lengths (#1424)
Xiph comment lengths are read from the file. Without checking them
against the remaining packet, a wrapped length can reset the parser
position and make each field iteration copy the same payload.
Reject truncated vendor and field data before advancing the parser.
2026-08-18 07:07:33 +02:00