Files
taglib/taglib/fileref.cpp
T
Ryan FrancesconiandGitHub 275029408c 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.
2026-11-03 20:17:53 +01:00

697 lines
21 KiB
C++

/***************************************************************************
copyright : (C) 2002 - 2008 by Scott Wheeler
email : [email protected]
copyright : (C) 2010 by Alex Novichkov
email : [email protected]
(added APE file support)
***************************************************************************/
/***************************************************************************
* This library is free software; you can redistribute it and/or modify *
* it under the terms of the GNU Lesser General Public License version *
* 2.1 as published by the Free Software Foundation. *
* *
* This library is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; if not, write to the Free Software *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
* 02110-1301 USA *
* *
* Alternatively, this file is available under the Mozilla Public *
* License Version 1.1. You may obtain a copy of the License at *
* http://www.mozilla.org/MPL/ *
***************************************************************************/
#include "fileref.h"
#include <cstring>
#include <utility>
#include "taglib_config.h"
#include "tfilestream.h"
#include "tpropertymap.h"
#include "tstringlist.h"
#include "tvariant.h"
#include "tdebug.h"
#include "tagutils.h"
#include "mpegfile.h"
#ifdef TAGLIB_WITH_RIFF
#include "aifffile.h"
#include "wavfile.h"
#endif
#ifdef TAGLIB_WITH_APE
#include "apefile.h"
#include "mpcfile.h"
#include "wavpackfile.h"
#endif
#ifdef TAGLIB_WITH_ASF
#include "asffile.h"
#endif
#ifdef TAGLIB_WITH_VORBIS
#include "flacfile.h"
#include "speexfile.h"
#include "vorbisfile.h"
#include "oggflacfile.h"
#include "opusfile.h"
#endif
#ifdef TAGLIB_WITH_MOD
#include "itfile.h"
#include "modfile.h"
#include "xmfile.h"
#include "s3mfile.h"
#endif
#ifdef TAGLIB_WITH_MP4
#include "mp4file.h"
#endif
#ifdef TAGLIB_WITH_TRUEAUDIO
#include "trueaudiofile.h"
#endif
#ifdef TAGLIB_WITH_DSF
#include "dsffile.h"
#include "dsdifffile.h"
#endif
#ifdef TAGLIB_WITH_SHORTEN
#include "shortenfile.h"
#endif
#ifdef TAGLIB_WITH_MATROSKA
#include "matroskafile.h"
#endif
using namespace TagLib;
class FileRef::FileTypeResolver::FileTypeResolverPrivate
{
};
class FileRef::StreamTypeResolver::StreamTypeResolverPrivate
{
};
namespace
{
List<const FileRef::FileTypeResolver *> fileTypeResolvers;
// Detect the file type by user-defined resolvers.
File *detectByResolvers(FileName fileName, bool readAudioProperties,
AudioProperties::ReadStyle audioPropertiesStyle)
{
#ifdef _WIN32
if(::wcslen(fileName) == 0)
return nullptr;
#else
if(::strlen(fileName) == 0)
return nullptr;
#endif
for(const auto &resolver : std::as_const(fileTypeResolvers)) {
File *file = resolver->createFile(fileName, readAudioProperties, audioPropertiesStyle);
if(file)
return file;
}
return nullptr;
}
File *detectByResolvers(IOStream* stream, bool readAudioProperties,
AudioProperties::ReadStyle audioPropertiesStyle)
{
for(const auto &resolver : std::as_const(fileTypeResolvers)) {
if(auto streamResolver = dynamic_cast<const FileRef::StreamTypeResolver *>(resolver)) {
if(File *file = streamResolver->createFileFromStream(
stream, readAudioProperties, audioPropertiesStyle))
return file;
}
}
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,
AudioProperties::ReadStyle audioPropertiesStyle)
{
#ifdef _WIN32
const String s = stream->name().toString();
#else
const String s(stream->name());
#endif
String ext;
if(const int pos = s.rfind("."); pos != -1)
ext = s.substr(pos + 1).upper();
// If this list is updated, the method defaultFileExtensions() should also be
// updated. However at some point that list should be created at the same time
// that a default file type resolver is created.
if(ext.isEmpty())
return nullptr;
// .oga can be any audio in the Ogg container. So leave it to content-based detection.
File *file = nullptr;
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);
else if(ext == "OGA") {
/* .oga can be any audio in the Ogg container. First try FLAC, then Vorbis. */
file = new Ogg::FLAC::File(stream, readAudioProperties, audioPropertiesStyle);
if(!file->isValid()) {
delete file;
file = new Ogg::Vorbis::File(stream, readAudioProperties, audioPropertiesStyle);
}
}
else if(ext == "FLAC")
file = new FLAC::File(stream, readAudioProperties, audioPropertiesStyle);
else if(ext == "SPX")
file = new Ogg::Speex::File(stream, readAudioProperties, audioPropertiesStyle);
else if(ext == "OPUS")
file = new Ogg::Opus::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_APE
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") {
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")
file = new MP4::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_ASF
else if(ext == "WMA" || ext == "ASF")
file = new ASF::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_RIFF
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
else if(ext == "MOD" || ext == "MODULE" || ext == "NST" || ext == "WOW")
file = new Mod::File(stream, readAudioProperties, audioPropertiesStyle);
else if(ext == "S3M")
file = new S3M::File(stream, readAudioProperties, audioPropertiesStyle);
else if(ext == "IT")
file = new IT::File(stream, readAudioProperties, audioPropertiesStyle);
else if(ext == "XM")
file = new XM::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_DSF
else if(ext == "DSF")
file = new DSF::File(stream, readAudioProperties, audioPropertiesStyle);
else if(ext == "DFF" || ext == "DSDIFF")
file = new DSDIFF::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_SHORTEN
else if(ext == "SHN")
file = new Shorten::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_MATROSKA
else if(ext == "MKA" || ext == "MKV" || ext == "WEBM")
file = new Matroska::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
// if file is not valid, leave it to content-based detection.
if(file) {
if(file->isValid())
return file;
delete file;
}
return nullptr;
}
// Detect the file type based on the actual content of the stream.
File *detectByContent(IOStream *stream, bool readAudioProperties,
AudioProperties::ReadStyle audioPropertiesStyle)
{
File *file = nullptr;
if(false);
#ifdef TAGLIB_WITH_VORBIS
else if(Ogg::Vorbis::File::isSupported(stream))
file = new Ogg::Vorbis::File(stream, readAudioProperties, audioPropertiesStyle);
else if(Ogg::FLAC::File::isSupported(stream))
file = new Ogg::FLAC::File(stream, readAudioProperties, audioPropertiesStyle);
else if(FLAC::File::isSupported(stream))
file = new FLAC::File(stream, readAudioProperties, audioPropertiesStyle);
else if(Ogg::Speex::File::isSupported(stream))
file = new Ogg::Speex::File(stream, readAudioProperties, audioPropertiesStyle);
else if(Ogg::Opus::File::isSupported(stream))
file = new Ogg::Opus::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_APE
else if(MPC::File::isSupported(stream))
file = new MPC::File(stream, readAudioProperties, audioPropertiesStyle);
else if(WavPack::File::isSupported(stream))
file = new WavPack::File(stream, readAudioProperties, audioPropertiesStyle);
else if(APE::File::isSupported(stream))
file = new APE::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_TRUEAUDIO
else if(TrueAudio::File::isSupported(stream))
file = new TrueAudio::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_MP4
else if(MP4::File::isSupported(stream))
file = new MP4::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_ASF
else if(ASF::File::isSupported(stream))
file = new ASF::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_RIFF
else if(RIFF::AIFF::File::isSupported(stream))
file = new RIFF::AIFF::File(stream, readAudioProperties, audioPropertiesStyle);
else if(RIFF::WAV::File::isSupported(stream))
file = new RIFF::WAV::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_DSF
else if(DSF::File::isSupported(stream))
file = new DSF::File(stream, readAudioProperties, audioPropertiesStyle);
else if(DSDIFF::File::isSupported(stream))
file = new DSDIFF::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_SHORTEN
else if(Shorten::File::isSupported(stream))
file = new Shorten::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
#ifdef TAGLIB_WITH_MATROSKA
else if(Matroska::File::isSupported(stream))
file = new Matroska::File(stream, readAudioProperties, audioPropertiesStyle);
#endif
else if(MPEG::File::isSupported(stream))
file = new MPEG::File(stream, readAudioProperties, audioPropertiesStyle);
// isSupported() only does a quick check, so double check the file here.
if(file) {
if(file->isValid())
return file;
delete file;
}
return nullptr;
}
} // namespace
class FileRef::FileRefPrivate
{
public:
FileRefPrivate() = default;
~FileRefPrivate()
{
delete file;
delete stream;
}
FileRefPrivate(const FileRefPrivate &) = delete;
FileRefPrivate &operator=(const FileRefPrivate &) = delete;
bool isNull() const
{
return !file || !file->isValid();
}
bool isNullWithDebugMessage([[maybe_unused]] const String &methodName) const
{
if(isNull()) {
debug("FileRef::" + methodName + "() - Called without a valid file.");
return true;
}
return false;
}
File *file { nullptr };
IOStream *stream { nullptr };
};
////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////
FileRef::FileRef() :
d(std::make_shared<FileRefPrivate>())
{
}
FileRef::FileRef(FileName fileName, bool readAudioProperties,
AudioProperties::ReadStyle audioPropertiesStyle) :
d(std::make_shared<FileRefPrivate>())
{
parse(fileName, readAudioProperties, audioPropertiesStyle);
}
FileRef::FileRef(IOStream *stream, bool readAudioProperties, AudioProperties::ReadStyle audioPropertiesStyle) :
d(std::make_shared<FileRefPrivate>())
{
parse(stream, readAudioProperties, audioPropertiesStyle);
}
FileRef::FileRef(File *file) :
d(std::make_shared<FileRefPrivate>())
{
d->file = file;
}
FileRef::FileRef(const FileRef &) = default;
FileRef::~FileRef() = default;
Tag *FileRef::tag() const
{
if(d->isNullWithDebugMessage(__func__)) {
return nullptr;
}
return d->file->tag();
}
PropertyMap FileRef::properties() const
{
if(d->isNullWithDebugMessage(__func__)) {
return PropertyMap();
}
return d->file->properties();
}
void FileRef::removeUnsupportedProperties(const StringList& properties)
{
if(d->isNullWithDebugMessage(__func__)) {
return;
}
return d->file->removeUnsupportedProperties(properties);
}
PropertyMap FileRef::setProperties(const PropertyMap &properties)
{
if(d->isNullWithDebugMessage(__func__)) {
return PropertyMap();
}
return d->file->setProperties(properties);
}
StringList FileRef::complexPropertyKeys() const
{
if(d->isNullWithDebugMessage(__func__)) {
return StringList();
}
return d->file->complexPropertyKeys();
}
List<VariantMap> FileRef::complexProperties(const String &key) const
{
if(d->isNullWithDebugMessage(__func__)) {
return List<VariantMap>();
}
return d->file->complexProperties(key);
}
bool FileRef::setComplexProperties(const String &key, const List<VariantMap> &value)
{
if(d->isNullWithDebugMessage(__func__)) {
return false;
}
return d->file->setComplexProperties(key, value);
}
AudioProperties *FileRef::audioProperties() const
{
if(d->isNullWithDebugMessage(__func__)) {
return nullptr;
}
return d->file->audioProperties();
}
File *FileRef::file() const
{
return d->file;
}
bool FileRef::save()
{
if(d->isNullWithDebugMessage(__func__)) {
return false;
}
return d->file->save();
}
const FileRef::FileTypeResolver *FileRef::addFileTypeResolver(const FileRef::FileTypeResolver *resolver) // static
{
fileTypeResolvers.prepend(resolver);
return resolver;
}
void FileRef::clearFileTypeResolvers() // static
{
fileTypeResolvers.clear();
}
StringList FileRef::defaultFileExtensions()
{
StringList l;
l.append("mp3");
l.append("mp2");
l.append("aac");
#ifdef TAGLIB_WITH_VORBIS
l.append("ogg");
l.append("flac");
l.append("oga");
l.append("opus");
l.append("spx");
#endif
#ifdef TAGLIB_WITH_APE
l.append("mpc");
l.append("wv");
l.append("ape");
#endif
#ifdef TAGLIB_WITH_TRUEAUDIO
l.append("tta");
#endif
#ifdef TAGLIB_WITH_MP4
l.append("m4a");
l.append("m4r");
l.append("m4b");
l.append("m4p");
l.append("3g2");
l.append("mp4");
l.append("m4v");
#endif
#ifdef TAGLIB_WITH_ASF
l.append("wma");
l.append("asf");
#endif
#ifdef TAGLIB_WITH_RIFF
l.append("aif");
l.append("aiff");
l.append("afc");
l.append("aifc");
l.append("wav");
#endif
#ifdef TAGLIB_WITH_MOD
l.append("mod");
l.append("module"); // alias for "mod"
l.append("nst"); // alias for "mod"
l.append("wow"); // alias for "mod"
l.append("s3m");
l.append("it");
l.append("xm");
#endif
#ifdef TAGLIB_WITH_DSF
l.append("dsf");
l.append("dff");
l.append("dsdiff"); // alias for "dff"
#endif
#ifdef TAGLIB_WITH_SHORTEN
l.append("shn");
#endif
#ifdef TAGLIB_WITH_MATROSKA
l.append("mkv");
l.append("mka");
l.append("webm");
#endif
return l;
}
bool FileRef::isNull() const
{
return d->isNull();
}
FileRef &FileRef::operator=(const FileRef &) = default;
void FileRef::swap(FileRef &ref) noexcept
{
using std::swap;
swap(d, ref.d);
}
bool FileRef::operator==(const FileRef &ref) const
{
return ref.d->file == d->file;
}
bool FileRef::operator!=(const FileRef &ref) const
{
return ref.d->file != d->file;
}
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////
void FileRef::parse(FileName fileName, bool readAudioProperties,
AudioProperties::ReadStyle audioPropertiesStyle)
{
// Try user-defined resolvers.
d->file = detectByResolvers(fileName, readAudioProperties, audioPropertiesStyle);
if(d->file)
return;
// Try to resolve file types based on the file extension.
d->stream = new FileStream(fileName);
d->file = detectByExtension(d->stream, readAudioProperties, audioPropertiesStyle);
if(d->file)
return;
// At last, try to resolve file types based on the actual content.
// A parser tried for the extension may have left the stream anywhere.
d->stream->seek(0);
d->file = detectByContent(d->stream, readAudioProperties, audioPropertiesStyle);
if(d->file)
return;
// Stream have to be closed here if failed to resolve file types.
delete d->stream;
d->stream = nullptr;
}
void FileRef::parse(IOStream *stream, bool readAudioProperties,
AudioProperties::ReadStyle audioPropertiesStyle)
{
// Try user-defined stream resolvers.
d->file = detectByResolvers(stream, readAudioProperties, audioPropertiesStyle);
if(d->file)
return;
// Try user-defined resolvers.
d->file = detectByResolvers(stream->name(), readAudioProperties, audioPropertiesStyle);
if(d->file)
return;
// Try to resolve file types based on the file extension.
d->file = detectByExtension(stream, readAudioProperties, audioPropertiesStyle);
if(d->file)
return;
// At last, try to resolve file types based on the actual content of the file.
// A parser tried for the extension may have left the stream anywhere.
stream->seek(0);
d->file = detectByContent(stream, readAudioProperties, audioPropertiesStyle);
}
FileRef::FileTypeResolver::FileTypeResolver() = default;
FileRef::FileTypeResolver::~FileTypeResolver() = default;
FileRef::StreamTypeResolver::StreamTypeResolver() = default;
FileRef::StreamTypeResolver::~StreamTypeResolver() = default;