diff --git a/simgear/CMakeLists.txt b/simgear/CMakeLists.txt index 072e1a65..18c2e04c 100644 --- a/simgear/CMakeLists.txt +++ b/simgear/CMakeLists.txt @@ -5,6 +5,7 @@ foreach( mylibfolder bucket bvh debug + embedded_resources ephemeris io magvar diff --git a/simgear/embedded_resources/CMakeLists.txt b/simgear/embedded_resources/CMakeLists.txt new file mode 100644 index 00000000..8ae2253b --- /dev/null +++ b/simgear/embedded_resources/CMakeLists.txt @@ -0,0 +1,14 @@ +include (SimGearComponent) + +set(HEADERS EmbeddedResource.hxx EmbeddedResourceManager.hxx) +set(SOURCES EmbeddedResource.cxx EmbeddedResourceManager.cxx) + +simgear_component(embedded_resources embedded_resources + "${SOURCES}" "${HEADERS}") + +if(ENABLE_TESTS) + add_executable(test_embedded_resources embedded_resources_test.cxx) + target_link_libraries(test_embedded_resources ${TEST_LIBS}) + add_test(embedded_resources + ${EXECUTABLE_OUTPUT_PATH}/test_embedded_resources) +endif(ENABLE_TESTS) diff --git a/simgear/embedded_resources/EmbeddedResource.cxx b/simgear/embedded_resources/EmbeddedResource.cxx new file mode 100644 index 00000000..c7d1f4f3 --- /dev/null +++ b/simgear/embedded_resources/EmbeddedResource.cxx @@ -0,0 +1,265 @@ +// -*- coding: utf-8 -*- +// +// EmbeddedResource.cxx --- Class for pointing to/accessing an embedded resource +// Copyright (C) 2017 Florent Rougon +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// 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 +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library 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. + +#include + +#include +#include +#include // std::streamsize +#include +#include // std::unique_ptr +#include // std::move() +#include // std::min() +#include // std::numeric_limits +#include // std::size_t, std::ptrdiff_t + +#include +#include +#include +#include "EmbeddedResource.hxx" + +using std::string; +using std::unique_ptr; + +// Inspired by +template +static unique_ptr static_unique_ptr_cast(unique_ptr p) +{ + auto d = static_cast(p.release()); + return unique_ptr(d); +} + + +namespace simgear +{ + +// *************************************************************************** +// * AbstractEmbeddedResource class * +// *************************************************************************** + +AbstractEmbeddedResource::AbstractEmbeddedResource(const char *data, + std::size_t size) + : _data(data), + _size(size) +{ } + +const char *AbstractEmbeddedResource::rawPtr() const +{ + return _data; +} + +std::size_t AbstractEmbeddedResource::rawSize() const +{ + return _size; +} + +string AbstractEmbeddedResource::str() const +{ + if (_size > std::numeric_limits::max()) { + throw sg_range_exception( + "Resource too large to fit in an std::string (size: " + + std::to_string(_size) + " bytes)"); + } + + return string(_data, _size); +} + +// *************************************************************************** +// * RawEmbeddedResource class * +// *************************************************************************** + +RawEmbeddedResource::RawEmbeddedResource(const char *data, std::size_t size) + : AbstractEmbeddedResource(data, size) +{ } + +AbstractEmbeddedResource::CompressionType +RawEmbeddedResource::compressionType() const +{ + return AbstractEmbeddedResource::CompressionType::NONE; +} + +string RawEmbeddedResource::compressionDescr() const +{ + return string("none"); +} + +unique_ptr RawEmbeddedResource::streambuf() const +{ + // This is a read-only variant of CharArrayStreambuf + return unique_ptr( + new ROCharArrayStreambuf(rawPtr(), rawSize())); +} + +unique_ptr RawEmbeddedResource::istream() const +{ + return unique_ptr(new CharArrayIStream(rawPtr(), rawSize())); +} + +// *************************************************************************** +// * ZlibEmbeddedResource class * +// *************************************************************************** + +ZlibEmbeddedResource::ZlibEmbeddedResource(const char *data, + std::size_t compressedSize, + std::size_t uncompressedSize) + : AbstractEmbeddedResource(data, compressedSize), + _uncompressedSize(uncompressedSize), + _inBuf(nullptr), + _inBufSize(262144), // adjusted below in the constructor body + _outBuf(nullptr), + _outBufSize(262144), + _putbackSize(0) // default for best performance +{ + static_assert(262144 <= std::numeric_limits::max(), + "The std::size_t type is unexpectedly small."); + // No need to use an input buffer (where compressed data chunks are put for + // zlib to read and decompress) larger than the whole compressed resource! + _inBufSize = std::min(rawSize(), _inBufSize); +} + +AbstractEmbeddedResource::CompressionType +ZlibEmbeddedResource::compressionType() const +{ return AbstractEmbeddedResource::CompressionType::ZLIB; } + +string ZlibEmbeddedResource::compressionDescr() const +{ return string("zlib"); } + +std::size_t ZlibEmbeddedResource::uncompressedSize() const +{ return _uncompressedSize; } + +char* ZlibEmbeddedResource::getInputBufferStart() +{ return _inBuf; } + +void ZlibEmbeddedResource::setInputBufferStart(char* inBuf) +{ _inBuf = inBuf; } + +std::size_t ZlibEmbeddedResource::getInputBufferSize() +{ return _inBufSize; } + +void ZlibEmbeddedResource::setInputBufferSize(std::size_t size) +{ _inBufSize = size; } + +char* ZlibEmbeddedResource::getOutputBufferStart() +{ return _outBuf; } + +void ZlibEmbeddedResource::setOutputBufferStart(char* outBuf) +{ _outBuf = outBuf; } + +std::size_t ZlibEmbeddedResource::getOutputBufferSize() +{ return _outBufSize; } + +void ZlibEmbeddedResource::setOutputBufferSize(std::size_t size) +{ _outBufSize = size; } + +std::size_t ZlibEmbeddedResource::getPutbackSize() +{ return _putbackSize; } + +void ZlibEmbeddedResource::setPutbackSize(std::size_t size) +{ _putbackSize = size; } + +unique_ptr ZlibEmbeddedResource::streambuf() const +{ + unique_ptr rawReaderIStream( + new CharArrayIStream(rawPtr(), rawSize())); + + return unique_ptr( + new ZlibDecompressorIStreambuf( + std::move(rawReaderIStream), + SGPath(), // rawReaderIStream isn't bound to a file + ZLibCompressionFormat::ZLIB, + _inBuf, _inBufSize, _outBuf, _outBufSize, _putbackSize)); +} + +unique_ptr ZlibEmbeddedResource::istream() const +{ + unique_ptr rawReaderIStream( + new CharArrayIStream(rawPtr(), rawSize())); + + return unique_ptr( + new ZlibDecompressorIStream( + std::move(rawReaderIStream), + SGPath(), // rawReaderIStream isn't bound to a file + ZLibCompressionFormat::ZLIB, + _inBuf, _inBufSize, _outBuf, _outBufSize, _putbackSize)); +} + +std::string ZlibEmbeddedResource::str() const +{ + static constexpr std::size_t bufSize = 65536; + static_assert(bufSize <= std::numeric_limits::max(), + "Type std::streamsize is unexpectedly small"); + static_assert(bufSize <= std::numeric_limits::max(), + "Type std::string::size_type is unexpectedly small"); + unique_ptr buf(new char[bufSize]); + + auto decompressor = + static_unique_ptr_cast(istream()); + std::streamsize nbCharsRead; + string result; + + if (_uncompressedSize > std::numeric_limits::max()) { + throw sg_range_exception( + "Resource too large to fit in an std::string (uncompressed size: " + + std::to_string(_uncompressedSize) + " bytes)"); + } else { + result.reserve(static_cast(_uncompressedSize)); + } + + do { + decompressor->read(buf.get(), bufSize); + nbCharsRead = decompressor->gcount(); + + if (nbCharsRead > 0) { + result.append(buf.get(), nbCharsRead); + } + } while (*decompressor); + + // decompressor->fail() would *not* indicate an error, due to the semantics + // of std::istream::read(). + if (decompressor->bad()) { + throw sg_io_exception("Error while extracting a compressed resource"); + } + + return result; +} + +// *************************************************************************** +// * Stream insertion operators * +// *************************************************************************** +std::ostream& operator<<(std::ostream& os, + const RawEmbeddedResource& resource) +{ // This won't escape double quotes, backslashes, etc. in resource.str(). + return os << "RawEmbeddedResource:\n" + " compressionType = \"" << resource.compressionDescr() << "\"\n" + " rawPtr = " << (void*) resource.rawPtr() << "\n" + " rawSize = " << resource.rawSize(); +} + +std::ostream& operator<<(std::ostream& os, + const ZlibEmbeddedResource& resource) +{ // This won't escape double quotes, backslashes, etc. in resource.str(). + return os << "ZlibEmbeddedResource:\n" + " compressionType = \"" << resource.compressionDescr() << "\"\n" + " rawPtr = " << (void*) resource.rawPtr() << "\n" + " rawSize = " << resource.rawSize() << "\n" + " uncompressedSize = " << resource.uncompressedSize(); +} + +} // of namespace simgear diff --git a/simgear/embedded_resources/EmbeddedResource.hxx b/simgear/embedded_resources/EmbeddedResource.hxx new file mode 100644 index 00000000..668c0487 --- /dev/null +++ b/simgear/embedded_resources/EmbeddedResource.hxx @@ -0,0 +1,163 @@ +// -*- coding: utf-8 -*- +// +// EmbeddedResource.hxx --- Class for pointing to/accessing an embedded resource +// Copyright (C) 2017 Florent Rougon +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// 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 +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library 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. + +#ifndef FG_EMBEDDEDRESOURCE_HXX +#define FG_EMBEDDEDRESOURCE_HXX + +#include +#include +#include +#include // std::unique_ptr +#include // std::size_t, std::ptrdiff_t + +#include + + +namespace simgear +{ + +// Abstract base class for embedded resources +class AbstractEmbeddedResource +{ +public: + enum class CompressionType { + NONE = 0, + ZLIB + }; + + // Constructor. + // + // 'data' and 'size' indicate the resource contents. There is no requirement + // of null-termination, including for text data (given how + // EmbeddedResourceManager::getString() works, including a null terminator + // for text contents is actually counter-productive). The data may be of + // arbitrary type and size: binary, text, whatever. The constructed object + // (for derived classes since this one is abstract) does *not* hold a copy + // of the data, it just keeps a pointer to it and provides methods to access + // it. The data must therefore remain available as long as the object is in + // use---this class was designed for use with data stored in static + // variables. + explicit AbstractEmbeddedResource(const char *data, std::size_t size); + AbstractEmbeddedResource(const AbstractEmbeddedResource&) = default; + AbstractEmbeddedResource(AbstractEmbeddedResource&&) = default; + AbstractEmbeddedResource& operator=(const AbstractEmbeddedResource&) = default; + AbstractEmbeddedResource& operator=(AbstractEmbeddedResource&&) = default; + virtual ~AbstractEmbeddedResource() = default; + + // Return the pointer to beginning-of-resource contents---the same that was + // passed to the constructor. + const char *rawPtr() const; + // Return the resource size, as passed to the constructor. For a compressed + // resource, this is the compressed size; such resources provide an + // additional uncompressedSize() method. + std::size_t rawSize() const; + + // Return an std::string object containing a copy of the resource contents. + // For a compressed resource, this is the data obtained after decompression. + virtual std::string str() const; + // Return an std::streambuf instance providing read-only access to the + // resource contents (in uncompressed form for compressed resources). This + // allows memory-friendly access to large resources by enabling incremental + // processing with transparent decompression for compressed resources. + virtual std::unique_ptr streambuf() const = 0; + // Return an std::istream instance providing read-only access to the + // resource contents (in uncompressed form for compressed resources). + // + // The same remark as for streambuf() applies. std::istream is simply a + // higher-level interface than std::streambuf, otherwise both allow the same + // kind of processing. + virtual std::unique_ptr istream() const = 0; + + // Return the resource compression type. + virtual CompressionType compressionType() const = 0; + // Return a string description of the resource compression type. Examples: + // "none", "zlib". + virtual std::string compressionDescr() const = 0; + +private: + // Pointer to the start of resource contents + const char *_data; + // Size of resource contents, in bytes + std::size_t _size; +}; + +// Class to describe an uncompressed resource. See AbstractEmbeddedResource. +class RawEmbeddedResource : public AbstractEmbeddedResource +{ +public: + explicit RawEmbeddedResource(const char *data, std::size_t size); + + AbstractEmbeddedResource::CompressionType compressionType() const override; + std::string compressionDescr() const override; + + // The str() method is inherited from AbstractEmbeddedResource + std::unique_ptr streambuf() const override; + std::unique_ptr istream() const override; +}; + +// Class to describe a zlib-compressed resource. +// +// Instances of this class point to resource contents stored in the stream +// format documented in RFC 1950. +class ZlibEmbeddedResource : public AbstractEmbeddedResource +{ +public: + explicit ZlibEmbeddedResource(const char *data, std::size_t compressedSize, + std::size_t uncompressedSize); + + AbstractEmbeddedResource::CompressionType compressionType() const override; + std::string compressionDescr() const override; + // Return the resource uncompressed size, in bytes. + std::size_t uncompressedSize() const; + + std::string str() const override; + std::unique_ptr streambuf() const override; + std::unique_ptr istream() const override; + + // Getters and setters for parameters used in streambuf() and istream(). + // Calling any of the setters affects the subsequent streambuf() and + // istream() calls. + char* getInputBufferStart(); + void setInputBufferStart(char* inBuf); + std::size_t getInputBufferSize(); + void setInputBufferSize(std::size_t size); + char* getOutputBufferStart(); + void setOutputBufferStart(char* outBuf); + std::size_t getOutputBufferSize(); + void setOutputBufferSize(std::size_t size); + std::size_t getPutbackSize(); + void setPutbackSize(std::size_t size); + +private: + std::size_t _uncompressedSize; + char* _inBuf; + std::size_t _inBufSize; + char* _outBuf; + std::size_t _outBufSize; + std::size_t _putbackSize; +}; + +// These functions are essentially intended for troubleshooting purposes. +std::ostream& operator<<(std::ostream&, const RawEmbeddedResource&); +std::ostream& operator<<(std::ostream&, const ZlibEmbeddedResource&); + +} // of namespace simgear + +#endif // of FG_EMBEDDEDRESOURCE_HXX diff --git a/simgear/embedded_resources/EmbeddedResourceManager.cxx b/simgear/embedded_resources/EmbeddedResourceManager.cxx new file mode 100644 index 00000000..52690a5b --- /dev/null +++ b/simgear/embedded_resources/EmbeddedResourceManager.cxx @@ -0,0 +1,225 @@ +// -*- coding: utf-8 -*- +// +// EmbeddedResourceManager.cxx --- Manager class for resources embedded in an +// executable +// Copyright (C) 2017 Florent Rougon +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// 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 +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library 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. + +#include + +#include +#include // std::move() +#include +#include +#include +#include + +#include +#include "EmbeddedResource.hxx" +#include "EmbeddedResourceManager.hxx" +#include "EmbeddedResourceManager_private.hxx" + +using std::string; +using std::shared_ptr; +using std::unique_ptr; + +namespace simgear +{ + +static unique_ptr staticInstance; + +// *************************************************************************** +// * EmbeddedResourceManager::Impl * +// *************************************************************************** +EmbeddedResourceManager::Impl::Impl() +{ } + +string +EmbeddedResourceManager::Impl::getLocale() const +{ + return selectedLocale; +} + +string +EmbeddedResourceManager::Impl::selectLocale(const std::string& locale) +{ + string previousLocale = std::move(selectedLocale); + selectedLocale = locale; + // Update the list of resource pools to search when looking up a resource. + // This allows to optimize resource lookup: no need to parse, split and hash + // the same locale string every time to find the corresponding resource + // pools. + poolSearchList = listOfResourcePoolsToSearch(selectedLocale); + + return previousLocale; +} + +// Static method +std::vector +EmbeddedResourceManager::Impl::localesSearchList(const string& locale) +{ + std::vector result; + + if (locale.empty()) { + result.push_back(string()); // only the default locale + } else { + std::size_t sepIdx = locale.find_first_of('_'); + + if (sepIdx == string::npos) { + // Try the given “locale” first (e.g., fr), then the default locale + result = std::vector({locale, string()}); + } else { + string langCode = locale.substr(0, sepIdx); + // Try the given “locale” first (e.g., fr_FR), then the language code + // (e.g., fr) and finally the default locale + result = std::vector({locale, langCode, string()}); + } + } + + return result; +} + +auto +EmbeddedResourceManager::Impl::listOfResourcePoolsToSearch( + const string& locale) const + -> std::vector< shared_ptr > +{ + std::vector searchedLocales = localesSearchList(locale); + std::vector< shared_ptr > result; + + for (const string& loc: searchedLocales) { + auto poolPtrIt = localeToResourcePoolMap.find(loc); + // Don't store pointers to empty resource pools in 'result'. This + // optimizes resource fetching a little bit, but requires that all + // resources are added before this method is called. + if (poolPtrIt != localeToResourcePoolMap.end()) { + // Copy a shared_ptr + result.push_back(poolPtrIt->second); + } + } + + return result; +} + +// Static method +shared_ptr +EmbeddedResourceManager::Impl::lookupResourceInPools( + const string& virtualPath, + const std::vector< shared_ptr >& aPoolSearchList) +{ + // Search the provided resource pools in proper order. For instance, the one + // for 'fr_FR', then the one for 'fr' and finally the one for the default + // locale. Return the first resource found in one of these pools. + for (const shared_ptr& poolPtr: aPoolSearchList) { + auto resourcePtrIt = poolPtr->find(virtualPath); + + if (resourcePtrIt != poolPtr->end()) { + // Copy a shared_ptr + return resourcePtrIt->second; + } + } + + return shared_ptr(); // null shared_ptr object +} + +void +EmbeddedResourceManager::Impl::addResource( + const string& virtualPath, + unique_ptr resourcePtr, + const string& locale) +{ + // Find the resource pool corresponding to the specified locale + shared_ptr& resPoolPtr = localeToResourcePoolMap[locale]; + if (!resPoolPtr) { + resPoolPtr.reset(new ResourcePool()); + } + + auto emplaceRetval = resPoolPtr->emplace(virtualPath, std::move(resourcePtr)); + + if (!emplaceRetval.second) { + const string localeDescr = + (locale.empty()) ? "the default locale" : "locale '" + locale + "'"; + throw sg_error( + "Virtual path already in use for " + localeDescr + + " in the EmbeddedResourceManager: '" + virtualPath + "'"); + } +} + +// *************************************************************************** +// * EmbeddedResourceManager * +// *************************************************************************** +EmbeddedResourceManager::EmbeddedResourceManager() + : p(unique_ptr(new Impl)) +{ } + +const unique_ptr& +EmbeddedResourceManager::createInstance() +{ + staticInstance.reset(new EmbeddedResourceManager); + return staticInstance; +} + +const unique_ptr& +EmbeddedResourceManager::instance() +{ + return staticInstance; +} + +string +EmbeddedResourceManager::getLocale() const +{ + return p->getLocale(); +} + +string +EmbeddedResourceManager::selectLocale(const std::string& locale) +{ + return p->selectLocale(locale); +} + +void +EmbeddedResourceManager::addResource( + const string& virtualPath, + unique_ptr resourcePtr, + const string& locale) +{ + p->addResource(virtualPath, std::move(resourcePtr), locale); +} + +shared_ptr +EmbeddedResourceManager::getResourceOrNullPtr(const string& virtualPath) const +{ + // Failure would indicate that either no resource has been added, or + // selectLocale() hasn't been called. Remember that selectLocale() must be + // called after all resources have been added. + assert(!p->poolSearchList.empty()); + // Use the selected locale + return p->lookupResourceInPools(virtualPath, p->poolSearchList); +} + +shared_ptr +EmbeddedResourceManager::getResourceOrNullPtr(const string& virtualPath, + const string& locale) const +{ + // In this overload, we don't use the cached list of pools + // (p->poolSearchList), therefore this can be used to find a resource for + // any locale without any need to call selectLocale(). + return p->lookupResourceInPools(virtualPath, + p->listOfResourcePoolsToSearch(locale)); +} + +} // of namespace simgear diff --git a/simgear/embedded_resources/EmbeddedResourceManager.hxx b/simgear/embedded_resources/EmbeddedResourceManager.hxx new file mode 100644 index 00000000..d371bbd8 --- /dev/null +++ b/simgear/embedded_resources/EmbeddedResourceManager.hxx @@ -0,0 +1,200 @@ +// -*- coding: utf-8 -*- +// +// EmbeddedResourceManager.hxx --- Manager class for resources embedded in an +// executable +// Copyright (C) 2017 Florent Rougon +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// 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 +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library 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. + +#ifndef FG_EMBEDDEDRESOURCEMANAGER_HXX +#define FG_EMBEDDEDRESOURCEMANAGER_HXX + +#include +#include // std::unique_ptr, std::shared_ptr +#include +#include +#include // std::forward() +#include // std::size_t + +#include +#include "EmbeddedResource.hxx" + +namespace simgear +{ + +class EmbeddedResourceManager +{ +public: + EmbeddedResourceManager(const EmbeddedResourceManager&) = delete; + EmbeddedResourceManager& operator=(const EmbeddedResourceManager&) = delete; + EmbeddedResourceManager(EmbeddedResourceManager&&) = delete; + EmbeddedResourceManager& operator=(EmbeddedResourceManager&&) = delete; + // The instance is created by createInstance() -> private constructor + // but it should be deleted by its owning std::unique_ptr -> public destructor + ~EmbeddedResourceManager() = default; + + // Static creator + static const std::unique_ptr& createInstance(); + + // Singleton accessor + static const std::unique_ptr& instance(); + + // Return the currently-selected “locale”[*] for resource fetching. + // + // [*] For instance: std::string("") for the default locale + // std::string("fr") for French + // std::string("fr_FR") for French from France + std::string getLocale() const; + // Select the locale for which resources will be returned in the future, for + // the getResourceOrNullPtr(), getResource(), getString(), getStreambuf() + // and getIStream() overloads that don't have a 'locale' parameter. + // Return the previously-selected locale. + // + // This method *must* be called after all resources have been added. It can + // be called several times (typically, when the user changes the language + // for the whole application). If you just want to fetch one or two + // resources in a particular “locale” (language), it is simpler to use an + // overload of one of the getResourceOrNullPtr(), getResource(), ..., + // getIStream() methods that has a 'locale' parameter. + std::string selectLocale(const std::string& locale); + + // Add a resource for the specified locale to the embedded resource manager. + // This method acts as a sink for its second argument (the std::unique_ptr + // typically has to be std::move()d). If 'locale' is empty, the resource is + // added for the default locale. + void addResource(const std::string& virtualPath, + std::unique_ptr resourcePtr, + const std::string& locale = std::string()); + + // Get access to a resource. + // + // Fetch the resource for the selected locale (cf. selectLocale()), with + // fallback behavior[1]. If no resource is found for the given + // 'virtualPath', return a null + // std::shared_ptr. + // + // [1] This means that for instance, if the selected locale is 'es_ES', the + // resource is first looked up for the 'es_ES' “locale”; then, if not + // found, for 'es'; and finally, if still not found, for the default + // locale ''. + std::shared_ptr getResourceOrNullPtr( + const std::string& virtualPath) const; + // Same as the previous overload, except the resource is fetched for the + // specified locale (with fallback behavior) instead of for the selected + // locale. Use an empty 'locale' parameter to fetch the resource for the + // default locale. + std::shared_ptr getResourceOrNullPtr( + const std::string& virtualPath, + const std::string& locale) const; + + // Same overloads as for getResourceOrNullPtr(), except that if the resource + // isn't found, then an sg_exception is raised. These methods never return + // a null or empty std::shared_ptr. + template + std::shared_ptr getResource( + const std::string& virtualPath, Args&& ...args) const + { + const auto resPtr = getResourceOrNullPtr(virtualPath, + std::forward(args)...); + + if (!resPtr) { + throw sg_exception("No embedded resource found at virtual path '" + + virtualPath + "'"); + } + + return resPtr; + } + + // Get a resource contents in the form of an std::string. Raise an + // sg_exception if no resource is found for the specified 'virtualPath'. + // + // The returned std::string is a copy of the resource contents (possibly + // transparently decompressed, cf. simgear::ZlibEmbeddedResource). + template + std::string getString(const std::string& virtualPath, Args&& ...args) const + { + return getResource(virtualPath, std::forward(args)...)->str(); + } + + // Get access to a resource via an std::streambuf instance. Raise an + // sg_exception if no resource is found for the specified 'virtualPath'. + // + // This allows one to incrementally process the resource contents without + // ever making a copy of it (including incremental, transparent + // decompression if the resource happens to be compressed---cf. + // simgear::ZlibEmbeddedResource). + template + std::unique_ptr getStreambuf(const std::string& virtualPath, + Args&& ...args) const + { + return getResource(virtualPath, std::forward(args)...)->streambuf(); + } + + // Get access to a resource via an std::istream instance. Raise an + // sg_exception if no resource is found for the specified 'virtualPath'. + // + // The same remarks made for getStreambuf() apply here too. + template + std::unique_ptr getIStream(const std::string& virtualPath, + Args&& ...args) const + { + return getResource(virtualPath, std::forward(args)...)->istream(); + } + +private: + // Constructor called from createInstance() only + explicit EmbeddedResourceManager(); + + class Impl; + const std::unique_ptr p; // Pimpl idiom +}; + +// Explicit template instantiations +template +std::shared_ptr +EmbeddedResourceManager::getResource(const std::string& virtualPath, + const std::string& locale) const; +template +std::shared_ptr +EmbeddedResourceManager::getResource(const std::string& virtualPath) const; + +template +std::string +EmbeddedResourceManager::getString(const std::string& virtualPath, + const std::string& locale) const; +template +std::string +EmbeddedResourceManager::getString(const std::string& virtualPath) const; + +template +std::unique_ptr +EmbeddedResourceManager::getStreambuf(const std::string& virtualPath, + const std::string& locale) const; +template +std::unique_ptr +EmbeddedResourceManager::getStreambuf(const std::string& virtualPath) const; + +template +std::unique_ptr +EmbeddedResourceManager::getIStream(const std::string& virtualPath, + const std::string& locale) const; +template +std::unique_ptr +EmbeddedResourceManager::getIStream(const std::string& virtualPath) const; + +} // of namespace simgear + +#endif // of FG_EMBEDDEDRESOURCEMANAGER_HXX diff --git a/simgear/embedded_resources/EmbeddedResourceManager_private.hxx b/simgear/embedded_resources/EmbeddedResourceManager_private.hxx new file mode 100644 index 00000000..dfacc7c9 --- /dev/null +++ b/simgear/embedded_resources/EmbeddedResourceManager_private.hxx @@ -0,0 +1,94 @@ +// -*- coding: utf-8 -*- +// +// EmbeddedResourceManager_private.hxx --- Private implementation class for +// SimGear's EmbeddedResourceManager +// Copyright (C) 2017 Florent Rougon +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// 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 +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library 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. + +#ifndef FG_EMBEDDEDRESOURCEMANAGERPRIVATE_HXX +#define FG_EMBEDDEDRESOURCEMANAGERPRIVATE_HXX + +#include +#include // std::unique_ptr, std::shared_ptr +#include +#include + +#include "EmbeddedResource.hxx" + +namespace simgear +{ + +class EmbeddedResourceManager::Impl +{ +public: + explicit Impl(); + + // Each “locale” for which addResource() has been used has an associated + // resource pool, that is a sort of directory of all resources declared in + // this locale. The resource pool for a given locale (e.g., 'fr' or 'de_DE') + // maps resource virtual paths to the corresponding resource descriptors + // (via std::shared_ptr instances). + // + // Note: for optimal lookup performance, a tree would probably be better, + // since the expected use for each key here is to store a virtual + // path. But such an optimization is likely unneeded in most cases. + typedef std::unordered_map< std::string, + std::shared_ptr > + ResourcePool; + + // Return the list of “locales” to scan to implement fallback behavior when + // fetching a resource for the specified locale. This list will be searched + // from left to right. Examples: + // + // "" -> [""] + // "fr" -> ["fr", ""] + // "fr_FR" -> ["fr_FR", "fr", ""] + static std::vector localesSearchList(const std::string& locale); + // Same as localesSearchList(), except it returns the resource pools instead + // of the “locale” strings, and only those pools that are not empty. + std::vector< std::shared_ptr > listOfResourcePoolsToSearch( + const std::string& locale) const; + // Look up, in each of the pools referred to by 'poolSearchList', the + // resource associated to 'virtualPath'. Return the first match. + static std::shared_ptr lookupResourceInPools( + const std::string& virtualPath, + const std::vector< std::shared_ptr >& poolSearchList); + + // Implement the corresponding EmbeddedResourceManager public methods + std::string getLocale() const; + std::string selectLocale(const std::string& locale); + + // Ditto + void addResource(const std::string& virtualPath, + std::unique_ptr resourcePtr, + const std::string& locale); + + std::string selectedLocale; + // After each call to selectLocale(), we update this member to contain + // precisely the (ordered) list of pools to search for a resource in the + // selected “locale”. This allows relatively cheap resource lookups, + // assuming the desired “locale” doesn't change all the time. + std::vector< std::shared_ptr > poolSearchList; + + // Maps each “locale name” to the corresponding resource pool. + std::unordered_map< std::string, + std::shared_ptr > localeToResourcePoolMap; +}; + +} // of namespace simgear + +#endif // of FG_EMBEDDEDRESOURCEMANAGERPRIVATE_HXX diff --git a/simgear/embedded_resources/embedded_resources_test.cxx b/simgear/embedded_resources/embedded_resources_test.cxx new file mode 100644 index 00000000..878cfa10 --- /dev/null +++ b/simgear/embedded_resources/embedded_resources_test.cxx @@ -0,0 +1,412 @@ +// -*- coding: utf-8 -*- +// +// embedded_resources_test.cxx --- Automated tests for the embedded resources +// system in SimGear +// +// Copyright (C) 2017 Florent Rougon +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// 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 +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library 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. + +#include + +#include +#include +#include // std::streamsize +#include // std::cout (used for progress info) +#include // std::numeric_limits +#include // std::make_unsigned() +#include +#include // EXIT_SUCCESS +#include // std::size_t + +#include +#include +#include +#include +#include "EmbeddedResource.hxx" +#include "EmbeddedResourceManager.hxx" + +using std::cout; +using std::cerr; +using std::endl; +using std::string; +using std::unique_ptr; +using std::shared_ptr; +using simgear::AbstractEmbeddedResource; +using simgear::RawEmbeddedResource; +using simgear::ZlibEmbeddedResource; +using simgear::EmbeddedResourceManager; + +typedef typename std::make_unsigned::type uStreamSize; + +// Safely convert a non-negative std::streamsize into an std::size_t. If +// impossible, bail out. +std::size_t streamsizeToSize_t(std::streamsize n) +{ + SG_CHECK_GE(n, 0); + SG_CHECK_LE(static_cast(n), + std::numeric_limits::max()); + + return static_cast(n); +} + +// This array is null-terminated, but we'll declare the resource size as +// sizeof(res1Array) - 1 so that the null char is *not* part of it. This +// way allows one to treat text and binary resources exactly the same way, +// with the conversion to std::string via a simple +// std::string(res1Array, resourceSize) not producing a bizarre std::string +// instance whose last character would be '\0' (followed in memory by the same +// '\0' used as C-style string terminator this time!). +static const char res1Array[] = "This is a simple embedded resource test."; +static const char res1frArray[] = "Ceci est un petit test de ressource " + "embarquée."; +static const char res1fr_FRArray[] = "Ceci est un petit test de ressource " + "embarquée (variante fr_FR)."; +static const string lipsum = "\ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque congue ornare\n\ +congue. Mauris mollis est et porttitor condimentum. Vivamus laoreet blandit\n\ +odio eget consectetur. Etiam quis magna eu enim luctus pretium. In et\n\ +tristique nunc, non efficitur metus. Nullam efficitur tristique velit.\n\ +Praesent et luctus nunc. Mauris eros eros, rutrum at molestie quis, egestas et\n\ +lorem. Ut nulla turpis, eleifend sed mauris ac, faucibus molestie nulla.\n\ +Quisque viverra vel turpis nec efficitur. Proin non rutrum velit. Nam sodales\n\ +metus felis, eu pharetra velit posuere ut."; +// Should be enough to store the compressed lipsum (320 bytes are required +// with zlib 1.2.8, keeping some room to account for possible future format +// changes in the zlib output...). In any case, there is no risk of buffer +// overflow, because simgear::CharArrayOStream prevents this by design. +static char res2Array[350]; +static const char res2frArray[] = "Un lorem ipsum un peu plus court..."; + + +// Read data from a string and write it in compressed form to the specified +// buffer. +std::size_t writeCompressedDataToBuffer(const string& inputString, + char *outBuf, + std::size_t outBufSize) +{ + simgear::CharArrayOStream res2Writer(outBuf, outBufSize); + std::istringstream iss(inputString); + simgear::ZlibCompressorIStream compressor( + iss, + SGPath(), /* no associated file */ + 9 /* highest compression level */); + static constexpr std::size_t bufSize = 1024; + unique_ptr buf(new char[bufSize]); + std::size_t res2Size = 0; + + do { + compressor.read(buf.get(), bufSize); + std::streamsize nBytes = compressor.gcount(); + if (nBytes > 0) { // at least one char could be read + res2Writer.write(buf.get(), nBytes); + res2Size += nBytes; + } + } while (compressor && res2Writer); + + SG_VERIFY(compressor.eof()); // all the compressed data has been read + // This would fail (among other causes) if the output buffer were too small + // to hold all of the compressed data. + SG_VERIFY(res2Writer); + + return res2Size; +} + +void initResources() +{ + cout << "Creating the EmbeddedResourceManager instance and adding a few " + "resources to it" << endl; + const auto& resMgr = EmbeddedResourceManager::createInstance(); + + // The resource will *not* consider the null terminator to be in. + unique_ptr res1( + new RawEmbeddedResource(res1Array, sizeof(res1Array) - 1)); + resMgr->addResource("/path/to/resource1", std::move(res1)); + + unique_ptr res1fr( + new RawEmbeddedResource(res1frArray, sizeof(res1frArray) - 1)); + resMgr->addResource("/path/to/resource1", std::move(res1fr), "fr"); + + unique_ptr res1fr_FR( + new RawEmbeddedResource(res1fr_FRArray, sizeof(res1fr_FRArray) - 1)); + resMgr->addResource("/path/to/resource1", std::move(res1fr_FR), "fr_FR"); + + // Write the contents of 'lipsum' in compressed form to the 'res2Array' + // static buffer. + std::size_t res2Size = writeCompressedDataToBuffer(lipsum, res2Array, + sizeof(res2Array)); + // Now we have a compressed resource to work with, plus the corresponding + // uncompressed output -> perfect for tests! + unique_ptr res2( + new ZlibEmbeddedResource(res2Array, res2Size, lipsum.size())); + resMgr->addResource("/path/to/resource2", std::move(res2)); + + unique_ptr res2fr( + new RawEmbeddedResource(res2frArray, sizeof(res2frArray) - 1)); + resMgr->addResource("/path/to/resource2", std::move(res2fr), "fr"); + + // This method must be called after all resources have been added. Here, we + // select the default locale (typically, English). + resMgr->selectLocale(""); +} + +// Auxiliary function for test_RawEmbeddedResource() +void auxTest_RawEmbeddedResource_streambuf() +{ + cout << "Testing EmbeddedResourceManager::getStreambuf()" << endl; + const auto& resMgr = EmbeddedResourceManager::instance(); + + unique_ptr sbuf(resMgr->getStreambuf("/path/to/resource1")); + // Just to show an efficient algorithm. For real applications, use larger + // buffer sizes! + static constexpr std::size_t bufSize = 4; + unique_ptr buf(new char[bufSize]); // intermediate buffer + std::streamsize nbCharsRead; + string result; + + do { + nbCharsRead = sbuf->sgetn(buf.get(), bufSize); + // The conversion to std::size_t is safe because sbuf->sgetn() returned a + // non-negative value which, in this case, can't exceed bufSize. + result.append(buf.get(), streamsizeToSize_t((nbCharsRead))); + } while (nbCharsRead == bufSize); + + SG_CHECK_EQUAL(result, "This is a simple embedded resource test."); +} + +// Auxiliary function for test_RawEmbeddedResource() +void auxTest_RawEmbeddedResource_istream() +{ + cout << "Testing EmbeddedResourceManager::getIStream()" << endl; + const auto& resMgr = EmbeddedResourceManager::instance(); + + unique_ptr iStream(resMgr->getIStream("/path/to/resource1")); + // This is convenient, but be aware that still in 2017, some buggy C++ + // compilers don't allow the exception to be caught: cf. + // . + iStream->exceptions(std::ios_base::badbit); + // Just to show an efficient algorithm. For real applications, use larger + // buffer sizes! + static constexpr std::size_t bufSize = 4; + unique_ptr buf(new char[bufSize]); // intermediate buffer + string result; + + do { + iStream->read(buf.get(), bufSize); + result.append(buf.get(), iStream->gcount()); + } while (*iStream); // iStream *points* to an std::istream + + // 1) If set, badbit would have caused an exception to be raised (see above). + // 2) failbit doesn't necessarily indicate an error here: it is set as soon + // as the read() call can't provide the requested number of characters. + SG_VERIFY(iStream->eof() && !iStream->bad()); + SG_CHECK_EQUAL(result, "This is a simple embedded resource test."); +} + +void test_RawEmbeddedResource() +{ + cout << "Testing resource fetching methods of EmbeddedResourceManager with " + "a RawEmbeddedResource" << endl; + const auto& resMgr = EmbeddedResourceManager::instance(); + + SG_CHECK_EQUAL(resMgr->getString("/path/to/resource1"), + string("This is a simple embedded resource test.")); + + // Get a shared_ptr to a const AbstractEmbeddedResource + const auto res1abs = resMgr->getResource("/path/to/resource1"); + // Okay because we know this resource is not a compressed one + const auto res1 = + std::dynamic_pointer_cast(res1abs); + SG_VERIFY(res1); + + // Print a representation of the resource metadata + std::cout << "\n/path/to/resource1 -> " << *res1 << "\n\n"; + + // The following methods would work the same with res1abs + SG_CHECK_EQUAL_NOSTREAM(res1->compressionType(), + AbstractEmbeddedResource::CompressionType::NONE); + SG_CHECK_EQUAL(res1->compressionDescr(), "none"); + + SG_CHECK_EQUAL(res1->rawPtr(), res1Array); + SG_CHECK_EQUAL(res1->rawSize(), sizeof(res1Array) - 1); // see above + SG_CHECK_EQUAL(res1->str(), + string("This is a simple embedded resource test.")); + + auxTest_RawEmbeddedResource_streambuf(); + auxTest_RawEmbeddedResource_istream(); + + // Just reload and recheck the resource, because we can :) + SG_CHECK_EQUAL(resMgr->getString("/path/to/resource1"), + string("This is a simple embedded resource test.")); +} + +void test_ZlibEmbeddedResource() +{ + cout << "Testing resource fetching methods of EmbeddedResourceManager with " + "a ZlibEmbeddedResource" << endl; + const auto& resMgr = EmbeddedResourceManager::instance(); + + SG_CHECK_EQUAL(resMgr->getString("/path/to/resource2"), + lipsum); + + // Get a shared_ptr to a const AbstractEmbeddedResource + const auto res2abs = resMgr->getResource("/path/to/resource2"); + // Okay because we know this resource is a Zlib-compressed one + const auto res2 = + std::dynamic_pointer_cast(res2abs); + SG_VERIFY(res2); + + SG_CHECK_EQUAL(res2->uncompressedSize(), lipsum.size()); + + // Print a representation of the resource metadata + std::cout << "\n/path/to/resource2 -> " << *res2 << "\n\n"; + cout << "Resource 2 compression ratio: " << + static_cast(res2->uncompressedSize()) / + static_cast(res2->rawSize()) << "\n"; + + // Just reload and recheck the resource + SG_CHECK_EQUAL(resMgr->getString("/path/to/resource2"), lipsum); +} + +void test_getMissingResources() +{ + cout << "Testing the behavior of EmbeddedResourceManager when trying to " + "fetch inexistent resources" << endl; + const auto& resMgr = EmbeddedResourceManager::instance(); + SG_VERIFY(!resMgr->getResourceOrNullPtr("/inexistant/resource")); + + bool gotException = false; + try { + resMgr->getResource("/inexistant/resource"); + } catch (const sg_exception&) { + gotException = true; + } + SG_VERIFY(gotException); + + gotException = false; + try { + resMgr->getString("/other/inexistant/resource"); + } catch (const sg_exception&) { + gotException = true; + } + SG_VERIFY(gotException); +} + +void test_addAlreadyExistingResource() +{ + cout << "Testing the behavior of EmbeddedResourceManager when trying to " + "add an already existing resource" << endl; + const auto& resMgr = EmbeddedResourceManager::instance(); + + for (const string& locale: {"", "fr", "fr_FR"}) { + // For these tests, we don't care about the resource contents -> no need + // to substract 1 from the result of sizeof() as we did above. + unique_ptr someRes( + new RawEmbeddedResource(res1fr_FRArray, sizeof(res1fr_FRArray))); + + bool gotException = false; + try { + resMgr->addResource("/path/to/resource1", std::move(someRes), locale); + } catch (const sg_error&) { + gotException = true; + } + SG_VERIFY(gotException); + } +} + +void test_localeDependencyOfResourceFetching() +{ + cout << "Testing the locale-dependency of resource fetching from " + "EmbeddedResourceManager" << endl; + const auto& resMgr = EmbeddedResourceManager::instance(); + resMgr->selectLocale(""); // select the default locale + + SG_CHECK_EQUAL(resMgr->getString("/path/to/resource1"), + "This is a simple embedded resource test."); + + // Switch to the 'fr_FR' locale (French from France) + resMgr->selectLocale("fr_FR"); + SG_CHECK_EQUAL(resMgr->getString("/path/to/resource1"), + "Ceci est un petit test de ressource embarquée (variante " + "fr_FR)."); + + // This one is for the 'fr' “locale”, obtained as fallback since there is no + // resource mapped to /path/to/resource2 for the 'fr_FR' “locale”. + SG_CHECK_EQUAL(resMgr->getString("/path/to/resource2"), + "Un lorem ipsum un peu plus court..."); + + // Explicitly ask for the resource in the default locale + SG_CHECK_EQUAL(resMgr->getString("/path/to/resource1", ""), + "This is a simple embedded resource test."); + + // Switch to the 'fr' locale (French) + resMgr->selectLocale("fr"); + SG_CHECK_EQUAL(resMgr->getString("/path/to/resource1"), + "Ceci est un petit test de ressource embarquée."); + + // Explicitly ask for the resource in the 'fr_FR' locale + SG_CHECK_EQUAL(resMgr->getString("/path/to/resource1", "fr_FR"), + "Ceci est un petit test de ressource embarquée " + "(variante fr_FR)."); + + // Switch to the default locale + resMgr->selectLocale(""); + SG_CHECK_EQUAL(resMgr->getString("/path/to/resource1"), + "This is a simple embedded resource test."); + + // Explicitly ask for the resource in the 'fr' locale + SG_CHECK_EQUAL(resMgr->getString("/path/to/resource1", "fr"), + "Ceci est un petit test de ressource embarquée."); + + // Explicitly ask for the resource in the 'fr_FR' locale + SG_CHECK_EQUAL(resMgr->getString("/path/to/resource1", "fr_FR"), + "Ceci est un petit test de ressource embarquée " + "(variante fr_FR)."); + + // Explicitly ask for the resource in the default locale + SG_CHECK_EQUAL(resMgr->getString("/path/to/resource1", ""), + "This is a simple embedded resource test."); +} + +void test_getLocaleAndSelectLocale() +{ + cout << "Testing the getLocale() and selectLocale() methods of " + "EmbeddedResourceManager" << endl; + const auto& resMgr = EmbeddedResourceManager::instance(); + + for (const string& locale: {"", "fr", "fr_FR", "de_DE"}) { + // The important effects of setLocale() are tested in + // test_localeDependencyOfResourceFetching() + resMgr->selectLocale(locale); + SG_CHECK_EQUAL(resMgr->getLocale(), locale); + } +} + +int main(int argc, char **argv) +{ + // Initialize the EmbeddedResourceManager instance, add a few resources + // to it and call its selectLocale() method. + initResources(); + + test_RawEmbeddedResource(); + test_ZlibEmbeddedResource(); + test_getMissingResources(); + test_addAlreadyExistingResource(); + test_localeDependencyOfResourceFetching(); + test_getLocaleAndSelectLocale(); + + return EXIT_SUCCESS; +}