mirror of
https://github.com/taglib/taglib.git
synced 2026-11-05 22:18:50 -05:00
FileRef: Check the header before parsing a file by its extension (#1490)
The MPEG, MPC, WavPack, APE, TrueAudio, AIFF and WAV parsers accept another format's content as valid, so a misnamed file was read with no audio properties or tags, and saving it could damage it. These branches now check the header first and try content-based detection on a mismatch, falling back to the extension's parser if it finds nothing.
This commit is contained in:
+83
-14
@@ -38,6 +38,7 @@
|
||||
#include "tstringlist.h"
|
||||
#include "tvariant.h"
|
||||
#include "tdebug.h"
|
||||
#include "tagutils.h"
|
||||
#include "mpegfile.h"
|
||||
#ifdef TAGLIB_WITH_RIFF
|
||||
#include "aifffile.h"
|
||||
@@ -130,6 +131,46 @@ namespace
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
File *detectByContent(IOStream *stream, bool readAudioProperties,
|
||||
AudioProperties::ReadStyle audioPropertiesStyle);
|
||||
|
||||
// Some extension-based parsers accept the content of another format as valid.
|
||||
// They are only used when content-based detection finds nothing, so the stream
|
||||
// is rewound for them.
|
||||
|
||||
File *detectByContentAndRewind(IOStream *stream, bool readAudioProperties,
|
||||
AudioProperties::ReadStyle audioPropertiesStyle)
|
||||
{
|
||||
File *file = detectByContent(stream, readAudioProperties, audioPropertiesStyle);
|
||||
stream->seek(0);
|
||||
return file;
|
||||
}
|
||||
|
||||
// MPEG has no signature of its own, so a file named as MPEG is checked for
|
||||
// another container's signature instead, after any ID3v2 tag.
|
||||
|
||||
bool hasOtherContainerSignature(IOStream *stream)
|
||||
{
|
||||
static const ByteVector asfGuid("\x30\x26\xB2\x75\x8E\x66\xCF\x11"
|
||||
"\xA6\xD9\x00\xAA\x00\x62\xCE\x6C", 16);
|
||||
const ByteVector id = Utils::readHeader(stream, 24, true);
|
||||
|
||||
// A WAVE holding MPEG audio is read as MPEG, which also finds tags
|
||||
// appended after the RIFF data.
|
||||
if(id.startsWith("RIFF") && id.containsAt("WAVE", 8) && id.containsAt("fmt ", 12)) {
|
||||
const unsigned short format = id.toUShort(20, false);
|
||||
if(format == 0x0050 || format == 0x0055)
|
||||
return false;
|
||||
}
|
||||
|
||||
return id.startsWith("RIFF") || id.startsWith("RF64") || id.startsWith("BW64") ||
|
||||
id.startsWith("FORM") || id.containsAt("ftyp", 4) || id.startsWith("fLaC") ||
|
||||
id.startsWith("OggS") || id.startsWith("\x1A\x45\xDF\xA3") || id.startsWith(asfGuid) ||
|
||||
id.startsWith("MAC ") || id.startsWith("wvpk") || id.startsWith("TTA") ||
|
||||
id.startsWith("MPCK") || id.startsWith("MP+") || id.startsWith("DSD ") ||
|
||||
id.startsWith("FRM8") || id.startsWith("ajkg");
|
||||
}
|
||||
|
||||
// Detect the file type based on the file extension.
|
||||
|
||||
File* detectByExtension(IOStream *stream, bool readAudioProperties,
|
||||
@@ -156,8 +197,12 @@ namespace
|
||||
|
||||
File *file = nullptr;
|
||||
|
||||
if(ext == "MP3" || ext == "MP2" || ext == "AAC")
|
||||
file = new MPEG::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
if(ext == "MP3" || ext == "MP2" || ext == "AAC") {
|
||||
if(hasOtherContainerSignature(stream))
|
||||
file = detectByContentAndRewind(stream, readAudioProperties, audioPropertiesStyle);
|
||||
if(!file)
|
||||
file = new MPEG::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
}
|
||||
#ifdef TAGLIB_WITH_VORBIS
|
||||
else if(ext == "OGG")
|
||||
file = new Ogg::Vorbis::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
@@ -177,16 +222,32 @@ namespace
|
||||
file = new Ogg::Opus::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
#endif
|
||||
#ifdef TAGLIB_WITH_APE
|
||||
else if(ext == "MPC")
|
||||
file = new MPC::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if(ext == "WV")
|
||||
file = new WavPack::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if(ext == "APE")
|
||||
file = new APE::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if(ext == "MPC") {
|
||||
if(!MPC::File::isSupported(stream))
|
||||
file = detectByContentAndRewind(stream, readAudioProperties, audioPropertiesStyle);
|
||||
if(!file)
|
||||
file = new MPC::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
}
|
||||
else if(ext == "WV") {
|
||||
if(!WavPack::File::isSupported(stream))
|
||||
file = detectByContentAndRewind(stream, readAudioProperties, audioPropertiesStyle);
|
||||
if(!file)
|
||||
file = new WavPack::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
}
|
||||
else if(ext == "APE") {
|
||||
if(!APE::File::isSupported(stream))
|
||||
file = detectByContentAndRewind(stream, readAudioProperties, audioPropertiesStyle);
|
||||
if(!file)
|
||||
file = new APE::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
}
|
||||
#endif
|
||||
#ifdef TAGLIB_WITH_TRUEAUDIO
|
||||
else if(ext == "TTA")
|
||||
file = new TrueAudio::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if(ext == "TTA") {
|
||||
if(!TrueAudio::File::isSupported(stream))
|
||||
file = detectByContentAndRewind(stream, readAudioProperties, audioPropertiesStyle);
|
||||
if(!file)
|
||||
file = new TrueAudio::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
}
|
||||
#endif
|
||||
#ifdef TAGLIB_WITH_MP4
|
||||
else if(ext == "M4A" || ext == "M4R" || ext == "M4B" || ext == "M4P" || ext == "MP4" || ext == "3G2" || ext == "M4V")
|
||||
@@ -197,10 +258,18 @@ namespace
|
||||
file = new ASF::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
#endif
|
||||
#ifdef TAGLIB_WITH_RIFF
|
||||
else if(ext == "AIF" || ext == "AIFF" || ext == "AFC" || ext == "AIFC")
|
||||
file = new RIFF::AIFF::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if(ext == "WAV")
|
||||
file = new RIFF::WAV::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
else if(ext == "AIF" || ext == "AIFF" || ext == "AFC" || ext == "AIFC") {
|
||||
if(!RIFF::AIFF::File::isSupported(stream))
|
||||
file = detectByContentAndRewind(stream, readAudioProperties, audioPropertiesStyle);
|
||||
if(!file)
|
||||
file = new RIFF::AIFF::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
}
|
||||
else if(ext == "WAV") {
|
||||
if(!RIFF::WAV::File::isSupported(stream))
|
||||
file = detectByContentAndRewind(stream, readAudioProperties, audioPropertiesStyle);
|
||||
if(!file)
|
||||
file = new RIFF::WAV::File(stream, readAudioProperties, audioPropertiesStyle);
|
||||
}
|
||||
#endif
|
||||
#ifdef TAGLIB_WITH_MOD
|
||||
// module, nst and wow are possible but uncommon extensions
|
||||
|
||||
Binary file not shown.
@@ -147,6 +147,18 @@ class TestFileRef : public CppUnit::TestFixture
|
||||
#endif
|
||||
#ifdef TAGLIB_WITH_MATROSKA
|
||||
CPPUNIT_TEST(testMatroskaWithOtherExtension);
|
||||
#endif
|
||||
CPPUNIT_TEST(testOtherFormatsWithMPEGExtension);
|
||||
CPPUNIT_TEST(testEmptyFileWithMPEGExtension);
|
||||
CPPUNIT_TEST(testMPEGInWaveWithMPEGExtension);
|
||||
#ifdef TAGLIB_WITH_VORBIS
|
||||
CPPUNIT_TEST(testFLACWithMPEGExtensionSave);
|
||||
#endif
|
||||
#ifdef TAGLIB_WITH_APE
|
||||
CPPUNIT_TEST(testOtherFormatsWithAPEExtensions);
|
||||
#endif
|
||||
#ifdef TAGLIB_WITH_RIFF
|
||||
CPPUNIT_TEST(testOtherFormatsWithRIFFExtensions);
|
||||
#endif
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
@@ -434,6 +446,133 @@ public:
|
||||
}
|
||||
#endif
|
||||
|
||||
template <typename T>
|
||||
void fileRefWithOtherExtension(const string &filename, const string &ext,
|
||||
const string &otherExt)
|
||||
{
|
||||
ScopedFileCopy copy(filename, ext);
|
||||
const string newname = copy.fileName() + otherExt;
|
||||
CPPUNIT_ASSERT_EQUAL(0, std::rename(copy.fileName().c_str(), newname.c_str()));
|
||||
{
|
||||
FileRef f(newname.c_str());
|
||||
CPPUNIT_ASSERT(dynamic_cast<T *>(f.file()));
|
||||
CPPUNIT_ASSERT(!f.isNull());
|
||||
}
|
||||
{
|
||||
FileStream s(newname.c_str());
|
||||
FileRef f(&s);
|
||||
CPPUNIT_ASSERT(dynamic_cast<T *>(f.file()));
|
||||
CPPUNIT_ASSERT(!f.isNull());
|
||||
}
|
||||
deleteFile(newname);
|
||||
}
|
||||
|
||||
void testOtherFormatsWithMPEGExtension()
|
||||
{
|
||||
#ifdef TAGLIB_WITH_MP4
|
||||
fileRefWithOtherExtension<MP4::File>("has-tags", ".m4a", ".mp3");
|
||||
{
|
||||
ScopedFileCopy copy("has-tags", ".m4a");
|
||||
const string newname = copy.fileName() + ".mp3";
|
||||
CPPUNIT_ASSERT_EQUAL(0, std::rename(copy.fileName().c_str(), newname.c_str()));
|
||||
{
|
||||
FileRef f(newname.c_str());
|
||||
CPPUNIT_ASSERT(!f.tag()->isEmpty());
|
||||
}
|
||||
deleteFile(newname);
|
||||
}
|
||||
#endif
|
||||
#ifdef TAGLIB_WITH_VORBIS
|
||||
fileRefWithOtherExtension<FLAC::File>("no-tags", ".flac", ".mp3");
|
||||
#endif
|
||||
#ifdef TAGLIB_WITH_TRUEAUDIO
|
||||
fileRefWithOtherExtension<TrueAudio::File>("tagged", ".tta", ".aac");
|
||||
#endif
|
||||
}
|
||||
|
||||
void testEmptyFileWithMPEGExtension()
|
||||
{
|
||||
ScopedFileCopy copy("xing", ".mp3");
|
||||
const string name = copy.fileName();
|
||||
std::ofstream(name.c_str(), std::ios::binary | std::ios::trunc).close();
|
||||
{
|
||||
FileRef f(name.c_str());
|
||||
CPPUNIT_ASSERT(dynamic_cast<MPEG::File *>(f.file()));
|
||||
CPPUNIT_ASSERT(!f.isNull());
|
||||
f.tag()->setTitle("test title");
|
||||
CPPUNIT_ASSERT(f.save());
|
||||
}
|
||||
{
|
||||
FileRef f(name.c_str());
|
||||
CPPUNIT_ASSERT(dynamic_cast<MPEG::File *>(f.file()));
|
||||
CPPUNIT_ASSERT_EQUAL(String("test title"), f.tag()->title());
|
||||
}
|
||||
}
|
||||
|
||||
void testMPEGInWaveWithMPEGExtension()
|
||||
{
|
||||
// Only the MPEG parser reads the ID3v1 tag appended after the RIFF data.
|
||||
ScopedFileCopy copy("mpeg-in-wave", ".wav");
|
||||
const string newname = copy.fileName() + ".mp3";
|
||||
CPPUNIT_ASSERT_EQUAL(0, std::rename(copy.fileName().c_str(), newname.c_str()));
|
||||
{
|
||||
FileRef f(newname.c_str());
|
||||
CPPUNIT_ASSERT(dynamic_cast<MPEG::File *>(f.file()));
|
||||
CPPUNIT_ASSERT_EQUAL(String("RIFF MP3 Title"), f.tag()->title());
|
||||
}
|
||||
{
|
||||
FileStream s(newname.c_str());
|
||||
FileRef f(&s);
|
||||
CPPUNIT_ASSERT(dynamic_cast<MPEG::File *>(f.file()));
|
||||
CPPUNIT_ASSERT_EQUAL(String("RIFF MP3 Title"), f.tag()->title());
|
||||
}
|
||||
deleteFile(newname);
|
||||
}
|
||||
|
||||
#ifdef TAGLIB_WITH_VORBIS
|
||||
void testFLACWithMPEGExtensionSave()
|
||||
{
|
||||
ScopedFileCopy copy("no-tags", ".flac");
|
||||
const string newname = copy.fileName() + ".mp3";
|
||||
CPPUNIT_ASSERT_EQUAL(0, std::rename(copy.fileName().c_str(), newname.c_str()));
|
||||
{
|
||||
FileRef f(newname.c_str());
|
||||
f.tag()->setTitle("test title");
|
||||
CPPUNIT_ASSERT(f.save());
|
||||
}
|
||||
{
|
||||
FileStream s(newname.c_str());
|
||||
CPPUNIT_ASSERT_EQUAL(ByteVector("fLaC"), s.readBlock(4));
|
||||
}
|
||||
{
|
||||
FileRef f(newname.c_str());
|
||||
CPPUNIT_ASSERT(dynamic_cast<FLAC::File *>(f.file()));
|
||||
CPPUNIT_ASSERT_EQUAL(String("test title"), f.tag()->title());
|
||||
}
|
||||
deleteFile(newname);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef TAGLIB_WITH_APE
|
||||
void testOtherFormatsWithAPEExtensions()
|
||||
{
|
||||
fileRefWithOtherExtension<WavPack::File>("click", ".wv", ".mpc");
|
||||
fileRefWithOtherExtension<MPC::File>("click", ".mpc", ".ape");
|
||||
fileRefWithOtherExtension<APE::File>("mac-399", ".ape", ".wv");
|
||||
#ifdef TAGLIB_WITH_TRUEAUDIO
|
||||
fileRefWithOtherExtension<WavPack::File>("click", ".wv", ".tta");
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef TAGLIB_WITH_RIFF
|
||||
void testOtherFormatsWithRIFFExtensions()
|
||||
{
|
||||
fileRefWithOtherExtension<RIFF::AIFF::File>("noise", ".aif", ".wav");
|
||||
fileRefWithOtherExtension<RIFF::WAV::File>("empty", ".wav", ".aif");
|
||||
}
|
||||
#endif
|
||||
|
||||
void testUnsupported()
|
||||
{
|
||||
FileRef f1(TEST_FILE_PATH_C("no-extension"));
|
||||
|
||||
Reference in New Issue
Block a user