diff options
author | Tsuda Kageyu <tsuda.kageyu@gmail.com> | 2017-06-12 13:04:15 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-06-12 13:04:15 +0900 |
commit | 1fb310ec1fec60b9d6f8aae641905d1cd95b2069 (patch) | |
tree | 9116c154bdf832d5743139818cf6f66663a17796 | |
parent | c8bcd153fe4a1c9c792dd8cd404226b19a3fc1c7 (diff) | |
parent | c2fe93c12b70f3ae5604564558144282af1e48c2 (diff) |
Merge pull request #799 from TsudaKageyu/filetype-detection
Enable FileRef to detect file types by the actual content of a stream.
36 files changed, 663 insertions, 86 deletions
@@ -9,6 +9,7 @@ * Fixed handling of 'rate' atoms in MP4 files. * Fixed possible file corruptions when saving Ogg files. * Better handling of invalid UTF-8 sequences. + * Marked FileRef::create() deprecated. It returns null. * Several smaller bug fixes and performance improvements. TagLib 1.11.1 (Oct 24, 2016) diff --git a/taglib/ape/apefile.cpp b/taglib/ape/apefile.cpp index 9f298aaf..a10c1f64 100644 --- a/taglib/ape/apefile.cpp +++ b/taglib/ape/apefile.cpp @@ -84,6 +84,18 @@ public: }; //////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool APE::File::isSupported(IOStream *stream) +{ + // An APE file has an ID "MAC " somewhere. An ID3v2 tag may precede. + + const ByteVector buffer = Utils::readHeader(stream, bufferSize(), true); + return (buffer.find("MAC ") >= 0); +} + +//////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/ape/apefile.h b/taglib/ape/apefile.h index cfb19ff7..267778ba 100644 --- a/taglib/ape/apefile.h +++ b/taglib/ape/apefile.h @@ -211,6 +211,15 @@ namespace TagLib { */ bool hasID3v1Tag() const; + /*! + * Returns whether or not the given \a stream can be opened as an APE + * file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + private: File(const File &); File &operator=(const File &); diff --git a/taglib/asf/asffile.cpp b/taglib/asf/asffile.cpp index 8f395265..d5a80bca 100644 --- a/taglib/asf/asffile.cpp +++ b/taglib/asf/asffile.cpp @@ -27,6 +27,7 @@ #include <tbytevectorlist.h> #include <tpropertymap.h> #include <tstring.h> +#include <tagutils.h> #include "asffile.h" #include "asftag.h" @@ -474,6 +475,18 @@ void ASF::File::FilePrivate::CodecListObject::parse(ASF::File *file, unsigned in } //////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool ASF::File::isSupported(IOStream *stream) +{ + // An ASF file has to start with the designated GUID. + + const ByteVector id = Utils::readHeader(stream, 16, false); + return (id == headerGuid); +} + +//////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/asf/asffile.h b/taglib/asf/asffile.h index b674da79..05cf4ee2 100644 --- a/taglib/asf/asffile.h +++ b/taglib/asf/asffile.h @@ -115,6 +115,15 @@ namespace TagLib { */ virtual bool save(); + /*! + * Returns whether or not the given \a stream can be opened as an ASF + * file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + private: void read(); diff --git a/taglib/fileref.cpp b/taglib/fileref.cpp index dca69f6a..935c371b 100644 --- a/taglib/fileref.cpp +++ b/taglib/fileref.cpp @@ -28,6 +28,7 @@ ***************************************************************************/ #include <tfile.h> +#include <tfilestream.h> #include <tstring.h> #include <tdebug.h> #include <trefcounter.h> @@ -59,60 +60,148 @@ namespace typedef List<const FileRef::FileTypeResolver *> ResolverList; ResolverList fileTypeResolvers; - // Templatized internal functions. T should be String or IOStream*. + // Detect the file type by user-defined resolvers. - template <typename T> - FileName toFileName(T arg); - - template <> - FileName toFileName<IOStream *>(IOStream *arg) + File *detectByResolvers(FileName fileName, bool readAudioProperties, + AudioProperties::ReadStyle audioPropertiesStyle) { - return arg->name(); - } + ResolverList::ConstIterator it = fileTypeResolvers.begin(); + for(; it != fileTypeResolvers.end(); ++it) { + File *file = (*it)->createFile(fileName, readAudioProperties, audioPropertiesStyle); + if(file) + return file; + } - template <> - FileName toFileName<FileName>(FileName arg) - { - return arg; + return 0; } - template <typename T> - File *resolveFileType(T arg, bool readProperties, - AudioProperties::ReadStyle style); + // Detect the file type based on the file extension. - template <> - File *resolveFileType<IOStream *>(IOStream *arg, bool readProperties, - AudioProperties::ReadStyle style) + 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; + const int pos = s.rfind("."); + if(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 0; + + // .oga can be any audio in the Ogg container. So leave it to content-based detection. + + if(ext == "MP3") + return new MPEG::File(stream, ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle); + if(ext == "OGG") + return new Ogg::Vorbis::File(stream, readAudioProperties, audioPropertiesStyle); + if(ext == "FLAC") + return new FLAC::File(stream, ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle); + if(ext == "MPC") + return new MPC::File(stream, readAudioProperties, audioPropertiesStyle); + if(ext == "WV") + return new WavPack::File(stream, readAudioProperties, audioPropertiesStyle); + if(ext == "SPX") + return new Ogg::Speex::File(stream, readAudioProperties, audioPropertiesStyle); + if(ext == "OPUS") + return new Ogg::Opus::File(stream, readAudioProperties, audioPropertiesStyle); + if(ext == "TTA") + return new TrueAudio::File(stream, readAudioProperties, audioPropertiesStyle); + if(ext == "M4A" || ext == "M4R" || ext == "M4B" || ext == "M4P" || ext == "MP4" || ext == "3G2" || ext == "M4V") + return new MP4::File(stream, readAudioProperties, audioPropertiesStyle); + if(ext == "WMA" || ext == "ASF") + return new ASF::File(stream, readAudioProperties, audioPropertiesStyle); + if(ext == "AIF" || ext == "AIFF" || ext == "AFC" || ext == "AIFC") + return new RIFF::AIFF::File(stream, readAudioProperties, audioPropertiesStyle); + if(ext == "WAV") + return new RIFF::WAV::File(stream, readAudioProperties, audioPropertiesStyle); + if(ext == "APE") + return new APE::File(stream, readAudioProperties, audioPropertiesStyle); + // module, nst and wow are possible but uncommon extensions + if(ext == "MOD" || ext == "MODULE" || ext == "NST" || ext == "WOW") + return new Mod::File(stream, readAudioProperties, audioPropertiesStyle); + if(ext == "S3M") + return new S3M::File(stream, readAudioProperties, audioPropertiesStyle); + if(ext == "IT") + return new IT::File(stream, readAudioProperties, audioPropertiesStyle); + if(ext == "XM") + return new XM::File(stream, readAudioProperties, audioPropertiesStyle); + return 0; } - template <> - File *resolveFileType<FileName>(FileName arg, bool readProperties, - AudioProperties::ReadStyle style) + // Detect the file type based on the actual content of the stream. + + File *detectByContent(IOStream *stream, bool readAudioProperties, + AudioProperties::ReadStyle audioPropertiesStyle) { - ResolverList::ConstIterator it = fileTypeResolvers.begin(); - for(; it != fileTypeResolvers.end(); ++it) { - File *file = (*it)->createFile(arg, readProperties, style); - if(file) + File *file = 0; + + if(MPEG::File::isSupported(stream)) + file = new MPEG::File(stream, ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle); + 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, ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle); + 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(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); + else if(TrueAudio::File::isSupported(stream)) + file = new TrueAudio::File(stream, readAudioProperties, audioPropertiesStyle); + else if(MP4::File::isSupported(stream)) + file = new MP4::File(stream, readAudioProperties, audioPropertiesStyle); + else if(ASF::File::isSupported(stream)) + file = new ASF::File(stream, readAudioProperties, audioPropertiesStyle); + 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); + else if(APE::File::isSupported(stream)) + file = new APE::File(stream, readAudioProperties, audioPropertiesStyle); + + // isSupported() only does a quick check, so double check the file here. + + if(file) { + if(file->isValid()) return file; + else + delete file; } return 0; } - template <typename T> - File* createInternal(T arg, bool readAudioProperties, + // Internal function that supports FileRef::create(). + // This looks redundant, but necessary in order not to change the previous + // behavior of FileRef::create(). + + File* createInternal(FileName fileName, bool readAudioProperties, AudioProperties::ReadStyle audioPropertiesStyle) { - File *file = resolveFileType(arg, readAudioProperties, audioPropertiesStyle); + File *file = detectByResolvers(fileName, readAudioProperties, audioPropertiesStyle); if(file) return file; #ifdef _WIN32 - const String s = toFileName(arg).toString(); + const String s = fileName.toString(); #else - const String s(toFileName(arg)); + const String s(fileName); #endif String ext; @@ -120,56 +209,52 @@ namespace if(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 0; if(ext == "MP3") - return new MPEG::File(arg, ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle); + return new MPEG::File(fileName, ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle); if(ext == "OGG") - return new Ogg::Vorbis::File(arg, readAudioProperties, audioPropertiesStyle); + return new Ogg::Vorbis::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "OGA") { /* .oga can be any audio in the Ogg container. First try FLAC, then Vorbis. */ - File *file = new Ogg::FLAC::File(arg, readAudioProperties, audioPropertiesStyle); + File *file = new Ogg::FLAC::File(fileName, readAudioProperties, audioPropertiesStyle); if(file->isValid()) return file; delete file; - return new Ogg::Vorbis::File(arg, readAudioProperties, audioPropertiesStyle); + return new Ogg::Vorbis::File(fileName, readAudioProperties, audioPropertiesStyle); } if(ext == "FLAC") - return new FLAC::File(arg, ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle); + return new FLAC::File(fileName, ID3v2::FrameFactory::instance(), readAudioProperties, audioPropertiesStyle); if(ext == "MPC") - return new MPC::File(arg, readAudioProperties, audioPropertiesStyle); + return new MPC::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "WV") - return new WavPack::File(arg, readAudioProperties, audioPropertiesStyle); + return new WavPack::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "SPX") - return new Ogg::Speex::File(arg, readAudioProperties, audioPropertiesStyle); + return new Ogg::Speex::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "OPUS") - return new Ogg::Opus::File(arg, readAudioProperties, audioPropertiesStyle); + return new Ogg::Opus::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "TTA") - return new TrueAudio::File(arg, readAudioProperties, audioPropertiesStyle); + return new TrueAudio::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "M4A" || ext == "M4R" || ext == "M4B" || ext == "M4P" || ext == "MP4" || ext == "3G2" || ext == "M4V") - return new MP4::File(arg, readAudioProperties, audioPropertiesStyle); + return new MP4::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "WMA" || ext == "ASF") - return new ASF::File(arg, readAudioProperties, audioPropertiesStyle); + return new ASF::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "AIF" || ext == "AIFF" || ext == "AFC" || ext == "AIFC") - return new RIFF::AIFF::File(arg, readAudioProperties, audioPropertiesStyle); + return new RIFF::AIFF::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "WAV") - return new RIFF::WAV::File(arg, readAudioProperties, audioPropertiesStyle); + return new RIFF::WAV::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "APE") - return new APE::File(arg, readAudioProperties, audioPropertiesStyle); + return new APE::File(fileName, readAudioProperties, audioPropertiesStyle); // module, nst and wow are possible but uncommon extensions if(ext == "MOD" || ext == "MODULE" || ext == "NST" || ext == "WOW") - return new Mod::File(arg, readAudioProperties, audioPropertiesStyle); + return new Mod::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "S3M") - return new S3M::File(arg, readAudioProperties, audioPropertiesStyle); + return new S3M::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "IT") - return new IT::File(arg, readAudioProperties, audioPropertiesStyle); + return new IT::File(fileName, readAudioProperties, audioPropertiesStyle); if(ext == "XM") - return new XM::File(arg, readAudioProperties, audioPropertiesStyle); + return new XM::File(fileName, readAudioProperties, audioPropertiesStyle); return 0; } @@ -178,15 +263,18 @@ namespace class FileRef::FileRefPrivate : public RefCounter { public: - FileRefPrivate(File *f) : + FileRefPrivate() : RefCounter(), - file(f) {} + file(0), + stream(0) {} ~FileRefPrivate() { delete file; + delete stream; } - File *file; + File *file; + IOStream *stream; }; //////////////////////////////////////////////////////////////////////////////// @@ -194,24 +282,27 @@ public: //////////////////////////////////////////////////////////////////////////////// FileRef::FileRef() : - d(new FileRefPrivate(0)) + d(new FileRefPrivate()) { } FileRef::FileRef(FileName fileName, bool readAudioProperties, AudioProperties::ReadStyle audioPropertiesStyle) : - d(new FileRefPrivate(createInternal(fileName, readAudioProperties, audioPropertiesStyle))) + d(new FileRefPrivate()) { + parse(fileName, readAudioProperties, audioPropertiesStyle); } FileRef::FileRef(IOStream* stream, bool readAudioProperties, AudioProperties::ReadStyle audioPropertiesStyle) : - d(new FileRefPrivate(createInternal(stream, readAudioProperties, audioPropertiesStyle))) + d(new FileRefPrivate()) { + parse(stream, readAudioProperties, audioPropertiesStyle); } FileRef::FileRef(File *file) : - d(new FileRefPrivate(file)) + d(new FileRefPrivate()) { + d->file = file; } FileRef::FileRef(const FileRef &ref) : @@ -333,3 +424,51 @@ File *FileRef::create(FileName fileName, bool readAudioProperties, { return createInternal(fileName, readAudioProperties, audioPropertiesStyle); } + +//////////////////////////////////////////////////////////////////////////////// +// 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. + + 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 = 0; +} + +void FileRef::parse(IOStream *stream, bool readAudioProperties, + AudioProperties::ReadStyle audioPropertiesStyle) +{ + // User-defined resolvers won't work with a stream. + + // 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. + + d->file = detectByContent(stream, readAudioProperties, audioPropertiesStyle); +} diff --git a/taglib/fileref.h b/taglib/fileref.h index a12b1a9b..c36f54cb 100644 --- a/taglib/fileref.h +++ b/taglib/fileref.h @@ -274,8 +274,10 @@ namespace TagLib { bool readAudioProperties = true, AudioProperties::ReadStyle audioPropertiesStyle = AudioProperties::Average); - private: + void parse(FileName fileName, bool readAudioProperties, AudioProperties::ReadStyle audioPropertiesStyle); + void parse(IOStream *stream, bool readAudioProperties, AudioProperties::ReadStyle audioPropertiesStyle); + class FileRefPrivate; FileRefPrivate *d; }; diff --git a/taglib/flac/flacfile.cpp b/taglib/flac/flacfile.cpp index b6b72960..7f437194 100644 --- a/taglib/flac/flacfile.cpp +++ b/taglib/flac/flacfile.cpp @@ -96,6 +96,18 @@ public: }; //////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool FLAC::File::isSupported(IOStream *stream) +{ + // A FLAC file has an ID "fLaC" somewhere. An ID3v2 tag may precede. + + const ByteVector buffer = Utils::readHeader(stream, bufferSize(), true); + return (buffer.find("fLaC") >= 0); +} + +//////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/flac/flacfile.h b/taglib/flac/flacfile.h index 65d85679..645090e0 100644 --- a/taglib/flac/flacfile.h +++ b/taglib/flac/flacfile.h @@ -318,6 +318,15 @@ namespace TagLib { */ bool hasID3v2Tag() const; + /*! + * Returns whether or not the given \a stream can be opened as a FLAC + * file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + private: File(const File &); File &operator=(const File &); diff --git a/taglib/mp4/mp4file.cpp b/taglib/mp4/mp4file.cpp index 3733fb40..5ad8396d 100644 --- a/taglib/mp4/mp4file.cpp +++ b/taglib/mp4/mp4file.cpp @@ -26,6 +26,8 @@ #include <tdebug.h> #include <tstring.h> #include <tpropertymap.h> +#include <tagutils.h> + #include "mp4atom.h" #include "mp4tag.h" #include "mp4file.h" @@ -69,6 +71,22 @@ public: MP4::Properties *properties; }; +//////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool MP4::File::isSupported(IOStream *stream) +{ + // An MP4 file has to have an "ftyp" box first. + + const ByteVector id = Utils::readHeader(stream, 8, false); + return id.containsAt("ftyp", 4); +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + MP4::File::File(FileName file, bool readProperties, AudioProperties::ReadStyle) : TagLib::File(file), d(new FilePrivate()) diff --git a/taglib/mp4/mp4file.h b/taglib/mp4/mp4file.h index 3840bd02..8a46d17d 100644 --- a/taglib/mp4/mp4file.h +++ b/taglib/mp4/mp4file.h @@ -120,6 +120,15 @@ namespace TagLib { */ bool hasMP4Tag() const; + /*! + * Returns whether or not the given \a stream can be opened as an ASF + * file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + private: void read(bool readProperties); diff --git a/taglib/mpc/mpcfile.cpp b/taglib/mpc/mpcfile.cpp index daf24c8f..0ffaf893 100644 --- a/taglib/mpc/mpcfile.cpp +++ b/taglib/mpc/mpcfile.cpp @@ -76,6 +76,19 @@ public: }; //////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool MPC::File::isSupported(IOStream *stream) +{ + // A newer MPC file has to start with "MPCK" or "MP+", but older files don't + // have keys to do a quick check. + + const ByteVector id = Utils::readHeader(stream, 4, false); + return (id == "MPCK" || id.startsWith("MP+")); +} + +//////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/mpc/mpcfile.h b/taglib/mpc/mpcfile.h index 541724dc..89a866e3 100644 --- a/taglib/mpc/mpcfile.h +++ b/taglib/mpc/mpcfile.h @@ -214,6 +214,15 @@ namespace TagLib { */ bool hasAPETag() const; + /*! + * Returns whether or not the given \a stream can be opened as an MPC + * file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + private: File(const File &); File &operator=(const File &); diff --git a/taglib/mpeg/mpegfile.cpp b/taglib/mpeg/mpegfile.cpp index 74bf779b..217b03dd 100644 --- a/taglib/mpeg/mpegfile.cpp +++ b/taglib/mpeg/mpegfile.cpp @@ -77,6 +77,55 @@ public: }; //////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +namespace +{ + // Dummy file class to make a stream work with MPEG::Header. + + class AdapterFile : public TagLib::File + { + public: + AdapterFile(IOStream *stream) : File(stream) {} + + Tag *tag() const { return 0; } + AudioProperties *audioProperties() const { return 0; } + bool save() { return false; } + }; +} + +bool MPEG::File::isSupported(IOStream *stream) +{ + if(!stream || !stream->isOpen()) + return false; + + // An MPEG file has MPEG frame headers. An ID3v2 tag may precede. + + // MPEG frame headers are really confusing with irrelevant binary data. + // So we check if a frame header is really valid. + + long headerOffset; + const ByteVector buffer = Utils::readHeader(stream, bufferSize(), true, &headerOffset); + + const long originalPosition = stream->tell(); + AdapterFile file(stream); + + for(unsigned int i = 0; i < buffer.size() - 1; ++i) { + if(isFrameSync(buffer, i)) { + const Header header(&file, headerOffset + i, true); + if(header.isValid()) { + stream->seek(originalPosition); + return true; + } + } + } + + stream->seek(originalPosition); + return false; +} + +//////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/mpeg/mpegfile.h b/taglib/mpeg/mpegfile.h index e9e97387..2d2dff00 100644 --- a/taglib/mpeg/mpegfile.h +++ b/taglib/mpeg/mpegfile.h @@ -370,6 +370,15 @@ namespace TagLib { */ bool hasAPETag() const; + /*! + * Returns whether or not the given \a stream can be opened as an MPEG + * file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + private: File(const File &); File &operator=(const File &); diff --git a/taglib/mpeg/mpegheader.cpp b/taglib/mpeg/mpegheader.cpp index 610b0320..5a5015d6 100644 --- a/taglib/mpeg/mpegheader.cpp +++ b/taglib/mpeg/mpegheader.cpp @@ -197,10 +197,8 @@ void MPEG::Header::parse(File *file, long offset, bool checkLength) d->version = Version2; else if(versionBits == 3) d->version = Version1; - else { - debug("MPEG::Header::parse() -- Invalid MPEG version bits."); + else return; - } // Set the MPEG layer @@ -212,10 +210,8 @@ void MPEG::Header::parse(File *file, long offset, bool checkLength) d->layer = 2; else if(layerBits == 3) d->layer = 1; - else { - debug("MPEG::Header::parse() -- Invalid MPEG layer bits."); + else return; - } d->protectionEnabled = (static_cast<unsigned char>(data[1] & 0x01) == 0); @@ -244,10 +240,8 @@ void MPEG::Header::parse(File *file, long offset, bool checkLength) d->bitrate = bitrates[versionIndex][layerIndex][bitrateIndex]; - if(d->bitrate == 0) { - debug("MPEG::Header::parse() -- Invalid bit rate."); + if(d->bitrate == 0) return; - } // Set the sample rate @@ -264,7 +258,6 @@ void MPEG::Header::parse(File *file, long offset, bool checkLength) d->sampleRate = sampleRates[d->version][samplerateIndex]; if(d->sampleRate == 0) { - debug("MPEG::Header::parse() -- Invalid sample rate."); return; } @@ -311,20 +304,16 @@ void MPEG::Header::parse(File *file, long offset, bool checkLength) file->seek(offset + d->frameLength); const ByteVector nextData = file->readBlock(4); - if(nextData.size() < 4) { - debug("MPEG::Header::parse() -- Could not read the next frame header."); + if(nextData.size() < 4) return; - } const unsigned int HeaderMask = 0xfffe0c00; const unsigned int header = data.toUInt(0, true) & HeaderMask; const unsigned int nextHeader = nextData.toUInt(0, true) & HeaderMask; - if(header != nextHeader) { - debug("MPEG::Header::parse() -- The next frame was not consistent with this frame."); + if(header != nextHeader) return; - } } // Now that we're done parsing, set this to be a valid frame. diff --git a/taglib/mpeg/mpegutils.h b/taglib/mpeg/mpegutils.h index 1cee918a..31b45a43 100644 --- a/taglib/mpeg/mpegutils.h +++ b/taglib/mpeg/mpegutils.h @@ -45,12 +45,12 @@ namespace TagLib * \note This does not check the length of the vector, since this is an * internal utility function. */ - inline bool isFrameSync(const ByteVector &bytes) + inline bool isFrameSync(const ByteVector &bytes, unsigned int offset = 0) { // 0xFF in the second byte is possible in theory, but it's very unlikely. - const unsigned char b1 = bytes[0]; - const unsigned char b2 = bytes[1]; + const unsigned char b1 = bytes[offset + 0]; + const unsigned char b2 = bytes[offset + 1]; return (b1 == 0xFF && b2 != 0xFF && (b2 & 0xE0) == 0xE0); } diff --git a/taglib/ogg/flac/oggflacfile.cpp b/taglib/ogg/flac/oggflacfile.cpp index fe4d8830..53d04508 100644 --- a/taglib/ogg/flac/oggflacfile.cpp +++ b/taglib/ogg/flac/oggflacfile.cpp @@ -27,6 +27,7 @@ #include <tstring.h> #include <tdebug.h> #include <tpropertymap.h> +#include <tagutils.h> #include <xiphcomment.h> #include "oggflacfile.h" @@ -66,6 +67,18 @@ public: }; //////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool Ogg::FLAC::File::isSupported(IOStream *stream) +{ + // An Ogg FLAC file has IDs "OggS" and "fLaC" somewhere. + + const ByteVector buffer = Utils::readHeader(stream, bufferSize(), false); + return (buffer.find("OggS") >= 0 && buffer.find("fLaC") >= 0); +} + +//////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/ogg/flac/oggflacfile.h b/taglib/ogg/flac/oggflacfile.h index 05762f9b..b2686e45 100644 --- a/taglib/ogg/flac/oggflacfile.h +++ b/taglib/ogg/flac/oggflacfile.h @@ -143,6 +143,14 @@ namespace TagLib { */ bool hasXiphComment() const; + /*! + * Check if the given \a stream can be opened as an Ogg FLAC file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + private: File(const File &); File &operator=(const File &); diff --git a/taglib/ogg/opus/opusfile.cpp b/taglib/ogg/opus/opusfile.cpp index ff1bfe2d..d4f191ad 100644 --- a/taglib/ogg/opus/opusfile.cpp +++ b/taglib/ogg/opus/opusfile.cpp @@ -30,6 +30,7 @@ #include <tstring.h> #include <tdebug.h> #include <tpropertymap.h> +#include <tagutils.h> #include "opusfile.h" @@ -54,6 +55,18 @@ public: }; //////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool Ogg::Opus::File::isSupported(IOStream *stream) +{ + // An Opus file has IDs "OggS" and "OpusHead" somewhere. + + const ByteVector buffer = Utils::readHeader(stream, bufferSize(), false); + return (buffer.find("OggS") >= 0 && buffer.find("OpusHead") >= 0); +} + +//////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/ogg/opus/opusfile.h b/taglib/ogg/opus/opusfile.h index b718f0d7..0e094eae 100644 --- a/taglib/ogg/opus/opusfile.h +++ b/taglib/ogg/opus/opusfile.h @@ -113,6 +113,15 @@ namespace TagLib { */ virtual bool save(); + /*! + * Returns whether or not the given \a stream can be opened as an Opus + * file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + private: File(const File &); File &operator=(const File &); diff --git a/taglib/ogg/speex/speexfile.cpp b/taglib/ogg/speex/speexfile.cpp index 7af71d50..b3c8a636 100644 --- a/taglib/ogg/speex/speexfile.cpp +++ b/taglib/ogg/speex/speexfile.cpp @@ -30,6 +30,7 @@ #include <tstring.h> #include <tdebug.h> #include <tpropertymap.h> +#include <tagutils.h> #include "speexfile.h" @@ -54,6 +55,18 @@ public: }; //////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool Ogg::Speex::File::isSupported(IOStream *stream) +{ + // A Speex file has IDs "OggS" and "Speex " somewhere. + + const ByteVector buffer = Utils::readHeader(stream, bufferSize(), false); + return (buffer.find("OggS") >= 0 && buffer.find("Speex ") >= 0); +} + +//////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/ogg/speex/speexfile.h b/taglib/ogg/speex/speexfile.h index 58b001dd..1be7113c 100644 --- a/taglib/ogg/speex/speexfile.h +++ b/taglib/ogg/speex/speexfile.h @@ -113,6 +113,15 @@ namespace TagLib { */ virtual bool save(); + /*! + * Returns whether or not the given \a stream can be opened as a Speex + * file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + private: File(const File &); File &operator=(const File &); diff --git a/taglib/ogg/vorbis/vorbisfile.cpp b/taglib/ogg/vorbis/vorbisfile.cpp index 2773bd3b..b4f221ab 100644 --- a/taglib/ogg/vorbis/vorbisfile.cpp +++ b/taglib/ogg/vorbis/vorbisfile.cpp @@ -28,10 +28,10 @@ #include <tstring.h> #include <tdebug.h> #include <tpropertymap.h> +#include <tagutils.h> #include "vorbisfile.h" - using namespace TagLib; class Vorbis::File::FilePrivate @@ -60,6 +60,18 @@ namespace TagLib { } //////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool Vorbis::File::isSupported(IOStream *stream) +{ + // An Ogg Vorbis file has IDs "OggS" and "\x01vorbis" somewhere. + + const ByteVector buffer = Utils::readHeader(stream, bufferSize(), false); + return (buffer.find("OggS") >= 0 && buffer.find("\x01vorbis") >= 0); +} + +//////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/ogg/vorbis/vorbisfile.h b/taglib/ogg/vorbis/vorbisfile.h index 9e71dcbe..04c0c04e 100644 --- a/taglib/ogg/vorbis/vorbisfile.h +++ b/taglib/ogg/vorbis/vorbisfile.h @@ -121,6 +121,14 @@ namespace TagLib { */ virtual bool save(); + /*! + * Check if the given \a stream can be opened as an Ogg Vorbis file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + private: File(const File &); File &operator=(const File &); diff --git a/taglib/riff/aiff/aifffile.cpp b/taglib/riff/aiff/aifffile.cpp index 1a29938c..4f9c868e 100644 --- a/taglib/riff/aiff/aifffile.cpp +++ b/taglib/riff/aiff/aifffile.cpp @@ -28,6 +28,7 @@ #include <id3v2tag.h> #include <tstringlist.h> #include <tpropertymap.h> +#include <tagutils.h> #include "aifffile.h" @@ -54,6 +55,18 @@ public: }; //////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool RIFF::AIFF::File::isSupported(IOStream *stream) +{ + // An AIFF file has to start with "FORM????AIFF" or "FORM????AIFC". + + const ByteVector id = Utils::readHeader(stream, 12, false); + return (id.startsWith("FORM") && (id.containsAt("AIFF", 8) || id.containsAt("AIFC", 8))); +} + +//////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/riff/aiff/aifffile.h b/taglib/riff/aiff/aifffile.h index a79d76b2..5ba1a279 100644 --- a/taglib/riff/aiff/aifffile.h +++ b/taglib/riff/aiff/aifffile.h @@ -126,6 +126,14 @@ namespace TagLib { */ bool hasID3v2Tag() const; + /*! + * Check if the given \a stream can be opened as an AIFF file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + private: File(const File &); File &operator=(const File &); diff --git a/taglib/riff/wav/wavfile.cpp b/taglib/riff/wav/wavfile.cpp index 79ff9167..0ebe21c3 100644 --- a/taglib/riff/wav/wavfile.cpp +++ b/taglib/riff/wav/wavfile.cpp @@ -23,10 +23,11 @@ * http://www.mozilla.org/MPL/ * ***************************************************************************/ -#include "tbytevector.h" -#include "tdebug.h" -#include "tstringlist.h" -#include "tpropertymap.h" +#include <tbytevector.h> +#include <tdebug.h> +#include <tstringlist.h> +#include <tpropertymap.h> +#include <tagutils.h> #include "wavfile.h" #include "id3v2tag.h" @@ -61,6 +62,18 @@ public: }; //////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool RIFF::WAV::File::isSupported(IOStream *stream) +{ + // A WAV file has to start with "RIFF????WAVE". + + const ByteVector id = Utils::readHeader(stream, 12, false); + return (id.startsWith("RIFF") && id.containsAt("WAVE", 8)); +} + +//////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/riff/wav/wavfile.h b/taglib/riff/wav/wavfile.h index 80f17a85..f6c190ed 100644 --- a/taglib/riff/wav/wavfile.h +++ b/taglib/riff/wav/wavfile.h @@ -175,6 +175,15 @@ namespace TagLib { */ bool hasInfoTag() const; + /*! + * Returns whether or not the given \a stream can be opened as a WAV + * file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + private: File(const File &); File &operator=(const File &); diff --git a/taglib/tagutils.cpp b/taglib/tagutils.cpp index dc047040..d6d92406 100644 --- a/taglib/tagutils.cpp +++ b/taglib/tagutils.cpp @@ -77,3 +77,29 @@ long Utils::findAPE(File *file, long id3v1Location) return -1; } + +ByteVector TagLib::Utils::readHeader(IOStream *stream, unsigned int length, + bool skipID3v2, long *headerOffset) +{ + if(!stream || !stream->isOpen()) + return ByteVector(); + + const long originalPosition = stream->tell(); + long bufferOffset = 0; + + if(skipID3v2) { + stream->seek(0); + const ByteVector data = stream->readBlock(ID3v2::Header::size()); + if(data.startsWith(ID3v2::Header::fileIdentifier())) + bufferOffset = ID3v2::Header(data).completeTagSize(); + } + + stream->seek(bufferOffset); + const ByteVector header = stream->readBlock(length); + stream->seek(originalPosition); + + if(headerOffset) + *headerOffset = bufferOffset; + + return header; +} diff --git a/taglib/tagutils.h b/taglib/tagutils.h index fb11d1e0..4488a32b 100644 --- a/taglib/tagutils.h +++ b/taglib/tagutils.h @@ -30,9 +30,12 @@ #ifndef DO_NOT_DOCUMENT // tell Doxygen not to document this header +#include <tbytevector.h> + namespace TagLib { class File; + class IOStream; namespace Utils { @@ -41,6 +44,9 @@ namespace TagLib { long findID3v2(File *file); long findAPE(File *file, long id3v1Location); + + ByteVector readHeader(IOStream *stream, unsigned int length, bool skipID3v2, + long *headerOffset = 0); } } diff --git a/taglib/trueaudio/trueaudiofile.cpp b/taglib/trueaudio/trueaudiofile.cpp index fc123ba3..e4de436e 100644 --- a/taglib/trueaudio/trueaudiofile.cpp +++ b/taglib/trueaudio/trueaudiofile.cpp @@ -74,6 +74,18 @@ public: }; //////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool TrueAudio::File::isSupported(IOStream *stream) +{ + // A TrueAudio file has to start with "TTA". An ID3v2 tag may precede. + + const ByteVector id = Utils::readHeader(stream, 3, true); + return (id == "TTA"); +} + +//////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/trueaudio/trueaudiofile.h b/taglib/trueaudio/trueaudiofile.h index 4bcb722a..3737ac63 100644 --- a/taglib/trueaudio/trueaudiofile.h +++ b/taglib/trueaudio/trueaudiofile.h @@ -235,6 +235,15 @@ namespace TagLib { */ bool hasID3v2Tag() const; + /*! + * Returns whether or not the given \a stream can be opened as a TrueAudio + * file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + private: File(const File &); File &operator=(const File &); diff --git a/taglib/wavpack/wavpackfile.cpp b/taglib/wavpack/wavpackfile.cpp index ef92f4bd..01bdba36 100644 --- a/taglib/wavpack/wavpackfile.cpp +++ b/taglib/wavpack/wavpackfile.cpp @@ -72,6 +72,18 @@ public: }; //////////////////////////////////////////////////////////////////////////////// +// static members +//////////////////////////////////////////////////////////////////////////////// + +bool WavPack::File::isSupported(IOStream *stream) +{ + // A WavPack file has to start with "wvpk". + + const ByteVector id = Utils::readHeader(stream, 4, false); + return (id == "wvpk"); +} + +//////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// diff --git a/taglib/wavpack/wavpackfile.h b/taglib/wavpack/wavpackfile.h index 7e0bd27a..ccc4ef6e 100644 --- a/taglib/wavpack/wavpackfile.h +++ b/taglib/wavpack/wavpackfile.h @@ -200,6 +200,14 @@ namespace TagLib { */ bool hasAPETag() const; + /*! + * Check if the given \a stream can be opened as a WavPack file. + * + * \note This method is designed to do a quick check. The result may + * not necessarily be correct. + */ + static bool isSupported(IOStream *stream); + private: File(const File &); File &operator=(const File &); diff --git a/tests/test_fileref.cpp b/tests/test_fileref.cpp index 1b899975..e766a891 100644 --- a/tests/test_fileref.cpp +++ b/tests/test_fileref.cpp @@ -39,9 +39,10 @@ #include <wavfile.h> #include <apefile.h> #include <aifffile.h> +#include <tfilestream.h> +#include <tbytevectorstream.h> #include <cppunit/extensions/HelperMacros.h> #include "utils.h" -#include <tfilestream.h> using namespace std; using namespace TagLib; @@ -79,6 +80,7 @@ class TestFileRef : public CppUnit::TestFixture CPPUNIT_TEST(testAIFF_1); CPPUNIT_TEST(testAIFF_2); CPPUNIT_TEST(testUnsupported); + CPPUNIT_TEST(testCreate); CPPUNIT_TEST(testFileResolver); CPPUNIT_TEST_SUITE_END(); @@ -129,6 +131,7 @@ public: CPPUNIT_ASSERT_EQUAL(f.tag()->track(), (unsigned int)7); CPPUNIT_ASSERT_EQUAL(f.tag()->year(), (unsigned int)2080); } + { FileStream fs(newname.c_str()); FileRef f(&fs); @@ -140,6 +143,64 @@ public: CPPUNIT_ASSERT_EQUAL(f.tag()->album(), String("ialbummmm")); CPPUNIT_ASSERT_EQUAL(f.tag()->track(), (unsigned int)7); CPPUNIT_ASSERT_EQUAL(f.tag()->year(), (unsigned int)2080); + f.tag()->setArtist("test artist"); + f.tag()->setTitle("test title"); + f.tag()->setGenre("Test!"); + f.tag()->setAlbum("albummmm"); + f.tag()->setTrack(5); + f.tag()->setYear(2020); + f.save(); + } + + ByteVector fileContent; + { + FileStream fs(newname.c_str()); + FileRef f(&fs); + CPPUNIT_ASSERT(dynamic_cast<T*>(f.file())); + CPPUNIT_ASSERT(!f.isNull()); + CPPUNIT_ASSERT_EQUAL(f.tag()->artist(), String("test artist")); + CPPUNIT_ASSERT_EQUAL(f.tag()->title(), String("test title")); + CPPUNIT_ASSERT_EQUAL(f.tag()->genre(), String("Test!")); + CPPUNIT_ASSERT_EQUAL(f.tag()->album(), String("albummmm")); + CPPUNIT_ASSERT_EQUAL(f.tag()->track(), (unsigned int)5); + CPPUNIT_ASSERT_EQUAL(f.tag()->year(), (unsigned int)2020); + + fs.seek(0); + fileContent = fs.readBlock(fs.length()); + } + + { + ByteVectorStream bs(fileContent); + FileRef f(&bs); + CPPUNIT_ASSERT(dynamic_cast<T*>(f.file())); + CPPUNIT_ASSERT(!f.isNull()); + CPPUNIT_ASSERT_EQUAL(f.tag()->artist(), String("test artist")); + CPPUNIT_ASSERT_EQUAL(f.tag()->title(), String("test title")); + CPPUNIT_ASSERT_EQUAL(f.tag()->genre(), String("Test!")); + CPPUNIT_ASSERT_EQUAL(f.tag()->album(), String("albummmm")); + CPPUNIT_ASSERT_EQUAL(f.tag()->track(), (unsigned int)5); + CPPUNIT_ASSERT_EQUAL(f.tag()->year(), (unsigned int)2020); + f.tag()->setArtist("ttest artist"); + f.tag()->setTitle("ytest title"); + f.tag()->setGenre("uTest!"); + f.tag()->setAlbum("ialbummmm"); + f.tag()->setTrack(7); + f.tag()->setYear(2080); + f.save(); + + fileContent = *bs.data(); + } + { + ByteVectorStream bs(fileContent); + FileRef f(&bs); + CPPUNIT_ASSERT(dynamic_cast<T*>(f.file())); + CPPUNIT_ASSERT(!f.isNull()); + CPPUNIT_ASSERT_EQUAL(f.tag()->artist(), String("ttest artist")); + CPPUNIT_ASSERT_EQUAL(f.tag()->title(), String("ytest title")); + CPPUNIT_ASSERT_EQUAL(f.tag()->genre(), String("uTest!")); + CPPUNIT_ASSERT_EQUAL(f.tag()->album(), String("ialbummmm")); + CPPUNIT_ASSERT_EQUAL(f.tag()->track(), (unsigned int)7); + CPPUNIT_ASSERT_EQUAL(f.tag()->year(), (unsigned int)2080); } } @@ -237,6 +298,19 @@ public: CPPUNIT_ASSERT(f2.isNull()); } + void testCreate() + { + // This is depricated. But worth it to test. + + File *f = FileRef::create(TEST_FILE_PATH_C("empty_vorbis.oga")); + CPPUNIT_ASSERT(dynamic_cast<Ogg::Vorbis::File*>(f)); + delete f; + + f = FileRef::create(TEST_FILE_PATH_C("xing.mp3")); + CPPUNIT_ASSERT(dynamic_cast<MPEG::File*>(f)); + delete f; + } + void testFileResolver() { { |