diff --git a/simgear/embedded_resources/CMakeLists.txt b/simgear/embedded_resources/CMakeLists.txt index 8ae2253b..ffe34cbe 100644 --- a/simgear/embedded_resources/CMakeLists.txt +++ b/simgear/embedded_resources/CMakeLists.txt @@ -1,7 +1,7 @@ include (SimGearComponent) -set(HEADERS EmbeddedResource.hxx EmbeddedResourceManager.hxx) -set(SOURCES EmbeddedResource.cxx EmbeddedResourceManager.cxx) +set(HEADERS EmbeddedResource.hxx EmbeddedResourceManager.hxx ResourceProxy.hxx) +set(SOURCES EmbeddedResource.cxx EmbeddedResourceManager.cxx ResourceProxy.cxx) simgear_component(embedded_resources embedded_resources "${SOURCES}" "${HEADERS}") diff --git a/simgear/embedded_resources/ResourceProxy.cxx b/simgear/embedded_resources/ResourceProxy.cxx new file mode 100644 index 00000000..d4623db8 --- /dev/null +++ b/simgear/embedded_resources/ResourceProxy.cxx @@ -0,0 +1,247 @@ +// -*- coding: utf-8 -*- +// +// ResourceProxy.cxx --- Unified access to real files or embedded resources +// 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 // std::find() +#include // std::streamsize +#include +#include // std::numeric_limits +#include +#include +#include +#include // std::size_t +#include + +#include +#include +#include +#include +#include "EmbeddedResourceManager.hxx" +#include "ResourceProxy.hxx" + +using std::string; +using std::vector; +using std::shared_ptr; +using std::unique_ptr; + + +namespace simgear +{ + +ResourceProxy::ResourceProxy(const SGPath& realRoot, const string& virtualRoot, + bool useEmbeddedResourcesByDefault) + : _realRoot(realRoot), + _virtualRoot(normalizeVirtualRoot(virtualRoot)), + _useEmbeddedResourcesByDefault(useEmbeddedResourcesByDefault) +{ } + +SGPath +ResourceProxy::getRealRoot() const +{ return _realRoot; } + +void +ResourceProxy::setRealRoot(const SGPath& realRoot) +{ _realRoot = realRoot; } + +string +ResourceProxy::getVirtualRoot() const +{ return _virtualRoot; } + +void +ResourceProxy::setVirtualRoot(const string& virtualRoot) +{ _virtualRoot = normalizeVirtualRoot(virtualRoot); } + +bool +ResourceProxy::getUseEmbeddedResources() const +{ return _useEmbeddedResourcesByDefault; } + +void +ResourceProxy::setUseEmbeddedResources(bool useEmbeddedResources) +{ _useEmbeddedResourcesByDefault = useEmbeddedResources; } + +// Static method: normalize the 'virtualRoot' argument of the constructor +// +// The argument must start with a slash and mustn't contain any '.' or '..' +// component. The return value never ends with a slash. +string +ResourceProxy::normalizeVirtualRoot(const string& path) +{ + ResourceProxy::checkPath(__func__, path, false /* allowStartWithColon */); + string res = path; + + // Make sure 'res' doesn't end with a '/'. + while (!res.empty() && res.back() == '/') { + res.pop_back(); // This will ease path concatenation + } + + return res; +} + +// Static method +void +ResourceProxy::checkPath(const string& callerMethod, const string& path, + bool allowStartWithColon) { + if (path.empty()) { + throw sg_format_exception( + "Invalid empty path for ResourceProxy::" + callerMethod + "(): '" + + path + "'", path); + } else if (allowStartWithColon && + !simgear::strutils::starts_with(path, ":/") && path[0] != '/') { + throw sg_format_exception( + "Invalid path for ResourceProxy::" + callerMethod + "(): it should " + "start with either ':/' or '/'", path); + } else if (!allowStartWithColon && path[0] != '/') { + throw sg_format_exception( + "Invalid path for ResourceProxy::" + callerMethod + "(): it should " + "start with a slash ('/')", path); + } else { + const vector components = simgear::strutils::split(path, "/"); + auto find = [&components](const string& s) -> bool { + return (std::find(components.begin(), components.end(), s) != + components.end()); + }; + + if (find(".") || find("..")) { + throw sg_format_exception( + "Invalid path for ResourceProxy::" + callerMethod + "(): " + "'.' and '..' components are not allowed", path); + } + } +} + +unique_ptr +ResourceProxy::getIStream(const string& path, bool fromEmbeddedResource) const +{ + ResourceProxy::checkPath(__func__, path, false /* allowStartWithColon */); + assert(!path.empty() && path.front() == '/'); + + if (fromEmbeddedResource) { + const auto& embeddedResMgr = simgear::EmbeddedResourceManager::instance(); + return embeddedResMgr->getIStream( + _virtualRoot + path, + ""); // fetch the default-locale version of the resource + } else { + const SGPath sgPath = _realRoot / path.substr(std::size_t(1)); + return unique_ptr(new sg_ifstream(sgPath)); + } +} + +unique_ptr +ResourceProxy::getIStream(const string& path) const +{ + return getIStream(path, _useEmbeddedResourcesByDefault); +} + +unique_ptr +ResourceProxy::getIStreamDecideOnPrefix(const string& path) const +{ + ResourceProxy::checkPath(__func__, path, true /* allowStartWithColon */); + + // 'path' is non-empty + if (path.front() == '/') { + return getIStream(path, false /* fromEmbeddedResource */); + } else if (path.front() == ':') { + assert(path.size() >= 2 && path[1] == '/'); + // Skip the leading ':' + return getIStream(path.substr(std::size_t(1)), + true /* fromEmbeddedResource */); + } else { + // The checkPath() call should make it impossible to reach this point. + std::abort(); + } +} + +string +ResourceProxy::getString(const string& path, bool fromEmbeddedResource) const +{ + string result; + + ResourceProxy::checkPath(__func__, path, false /* allowStartWithColon */); + assert(!path.empty() && path.front() == '/'); + + if (fromEmbeddedResource) { + const auto& embeddedResMgr = simgear::EmbeddedResourceManager::instance(); + // Fetch the default-locale version of the resource + result = embeddedResMgr->getString(_virtualRoot + path, ""); + } else { + const SGPath sgPath = _realRoot / path.substr(std::size_t(1)); + result.reserve(sgPath.sizeInBytes()); + const unique_ptr streamp = getIStream(path, + fromEmbeddedResource); + std::streamsize nbCharsRead; + + // Allocate a buffer + 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]); + + do { + streamp->read(buf.get(), bufSize); + nbCharsRead = streamp->gcount(); + + if (nbCharsRead > 0) { + result.append(buf.get(), nbCharsRead); + } + } while (*streamp); + + // streamp->fail() would *not* indicate an error, due to the semantics + // of std::istream::read(). + if (streamp->bad()) { + throw sg_io_exception("Error reading from file", sg_location(path)); + } + } + + return result; +} + +string +ResourceProxy::getString(const string& path) const +{ + return getString(path, _useEmbeddedResourcesByDefault); +} + +string +ResourceProxy::getStringDecideOnPrefix(const string& path) const +{ + string result; + + ResourceProxy::checkPath(__func__, path, true /* allowStartWithColon */); + + // 'path' is non-empty + if (path.front() == '/') { + result = getString(path, false /* fromEmbeddedResource */); + } else if (path.front() == ':') { + assert(path.size() >= 2 && path[1] == '/'); + // Skip the leading ':' + result = getString(path.substr(std::size_t(1)), + true /* fromEmbeddedResource */); + } else { + // The checkPath() call should make it impossible to reach this point. + std::abort(); + } + + return result; +} + +} // of namespace simgear diff --git a/simgear/embedded_resources/ResourceProxy.hxx b/simgear/embedded_resources/ResourceProxy.hxx new file mode 100644 index 00000000..79e560bb --- /dev/null +++ b/simgear/embedded_resources/ResourceProxy.hxx @@ -0,0 +1,152 @@ +// -*- coding: utf-8 -*- +// +// ResourceProxy.hxx --- Unified access to real files or embedded resources +// 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_RESOURCEPROXY_HXX +#define FG_RESOURCEPROXY_HXX + +#include +#include +#include + +#include + +// The ResourceProxy class allows one to access real files or embedded +// resources in a unified way. When using it, one can switch from one data +// source to the other with minimal code changes, possibly even at runtime (in +// which case there is obviously no code change at all). +// +// Sample usage of the ResourceProxy class (from FlightGear): +// +// simgear::ResourceProxy proxy(globals->get_fg_root(), "/FGData"); +// std::string s = proxy.getString("/some/path"); +// std::unique_ptr streamp = proxy.getIStream("/some/path"); +// +// The methods ResourceProxy::getString(const std::string& path) and +// ResourceProxy::getIStream(const std::string& path) decide whether to use +// embedded resources or real files depending on the boolean value passed to +// ResourceProxy::setUseEmbeddedResources() (also available as an optional +// parameter to the ResourceProxy constructor, defaulting to true). It is +// often most convenient to set this boolean once and then don't worry about +// it anymore (it is stored inside ResourceProxy). Otherwise, if you want to +// fetch resources some times from real files, other times from embedded +// resources, you may use the following methods: +// +// // Retrieve contents using embedded resources +// std:string s = proxy.getString("/some/path", true); +// std:string s = proxy.getStringDecideOnPrefix(":/some/path"); +// +// // Retrieve contents using real files +// std:string s = proxy.getString("/some/path", false); +// std:string s = proxy.getStringDecideOnPrefix("/some/path"); +// +// You can do exactly the same with ResourceProxy::getIStream() and +// ResourceProxy::getIStreamDecideOnPrefix(), except they return an +// std::unique_ptr instead of an std::string. +// +// Given how the 'proxy' object was constructed above, each of these calls +// will fetch data from either the real file $FG_ROOT/some/path or the +// embedded resource whose virtual path is '/FGData/some/path' (more +// precisely: the default-locale version of this resource). +// +// The 'path' argument of ResourceProxy's methods getString(), getIStream(), +// getStringDecideOnPrefix() and getIStreamDecideOnPrefix() must: +// +// - use UTF-8 encoding; +// +// - start with: +// * either '/' or ':/' for the 'DecideOnPrefix' variants; +// * only '/' for the other methods. +// +// - have its components separated by slashes; +// +// - not contain any '.' or '..' component. +// +// For the 'DecideOnPrefix' variants: +// +// - if the path starts with a slash ('/'), a real file access is done; +// +// - if, on the other hand, it starts with ':/', ResourceProxy uses the +// embedded resource whose virtual path is the specified path without its +// leading ':' (more precisely: the default-locale version of this +// resource). +namespace simgear +{ + +class ResourceProxy +{ +public: + // 'virtualRoot' must start with a '/', e.g: '/FGData'. Whether it ends + // with a '/' doesn't make a difference. + explicit ResourceProxy(const SGPath& realRoot, + const std::string& virtualRoot, + bool useEmbeddedResourcesByDefault = true); + + // Getters and setters for the corresponding data members + SGPath getRealRoot() const; + void setRealRoot(const SGPath& realRoot); + + std::string getVirtualRoot() const; + void setVirtualRoot(const std::string& virtualRoot); + + bool getUseEmbeddedResources() const; + void setUseEmbeddedResources(bool useEmbeddedResources); + + // Get an std::istream to read from a file or from an embedded resource. + std::unique_ptr + getIStream(const std::string& path, bool fromEmbeddedResource) const; + + std::unique_ptr + getIStream(const std::string& path) const; + + std::unique_ptr + getIStreamDecideOnPrefix(const std::string& path) const; + + // Get a file or embedded resource contents as a string. + std::string + getString(const std::string& path, bool fromEmbeddedResource) const; + + std::string + getString(const std::string& path) const; + + std::string + getStringDecideOnPrefix(const std::string& path) const; + +private: + // Check that 'path' starts with either ':/' or '/', and doesn't contain any + // '..' component ('path' may only start with ':/' if 'allowStartWithColon' + // is true). + static void + checkPath(const std::string& callerMethod, const std::string& path, + bool allowStartWithColon); + + // Normalize the 'virtualRoot' argument of the constructor. The argument + // must start with a '/' and mustn't contain any '.' or '..' component. The + // return value never ends with a '/'. + static std::string + normalizeVirtualRoot(const std::string& path); + + SGPath _realRoot; + std::string _virtualRoot; + bool _useEmbeddedResourcesByDefault; +}; + +} // of namespace simgear + +#endif // of FG_RESOURCEPROXY_HXX diff --git a/simgear/embedded_resources/embedded_resources_test.cxx b/simgear/embedded_resources/embedded_resources_test.cxx index 47bcd342..8f89d2d0 100644 --- a/simgear/embedded_resources/embedded_resources_test.cxx +++ b/simgear/embedded_resources/embedded_resources_test.cxx @@ -33,11 +33,14 @@ #include // std::size_t #include +#include #include #include +#include #include #include "EmbeddedResource.hxx" #include "EmbeddedResourceManager.hxx" +#include "ResourceProxy.hxx" using std::cout; using std::cerr; @@ -395,6 +398,102 @@ void test_getLocaleAndSelectLocale() } } +// Auxiliary function for test_ResourceProxy() +void auxTest_ResourceProxy_getIStream(unique_ptr iStream, + const string& contents) +{ + cout << "Testing ResourceProxy::getIStream()" << endl; + + iStream->exceptions(std::ios_base::badbit); + static constexpr std::size_t bufSize = 65536; + 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, contents); +} + +void test_ResourceProxy() +{ + cout << "Testing the ResourceProxy class" << endl; + + // Initialize stuff we need and create two files containing the contents of + // the default-locale version of two embedded resources: those with virtual + // paths '/path/to/resource1' and '/path/to/resource2'. + const auto& resMgr = EmbeddedResourceManager::instance(); + simgear::Dir tmpDir = simgear::Dir::tempDir("FlightGear"); + tmpDir.setRemoveOnDestroy(); + + const SGPath path1 = tmpDir.path() / "resource1"; + const SGPath path2 = tmpDir.path() / "resource2"; + + sg_ofstream out1(path1); + sg_ofstream out2(path2); + const string s1 = resMgr->getString("/path/to/resource1", ""); + // To make sure in these tests that we can tell whether something came from + // a real file or from an embedded resource. + const string rs1 = s1 + " from real file"; + const string rlipsum = lipsum + " from real file"; + + out1 << rs1; + out1.close(); + if (!out1) { + throw sg_io_exception("Error writing to file", sg_location(path1)); + } + + out2 << rlipsum; + out2.close(); + if (!out2) { + throw sg_io_exception("Error writing to file", sg_location(path2)); + } + + // 'proxy' defaults to using embedded resources + const simgear::ResourceProxy proxy(tmpDir.path(), + "/path/to", + true /* useEmbeddedResourcesByDefault */); + simgear::ResourceProxy rproxy(tmpDir.path(), "/path/to"); + // 'rproxy' defaults to using real files + rproxy.setUseEmbeddedResources(false); // could be done from the ctor too + + // Test ResourceProxy::getString() + SG_CHECK_EQUAL(proxy.getStringDecideOnPrefix("/resource1"), rs1); + SG_CHECK_EQUAL(proxy.getStringDecideOnPrefix(":/resource1"), s1); + SG_CHECK_EQUAL(proxy.getString("/resource1", false), rs1); + SG_CHECK_EQUAL(proxy.getString("/resource1", true), s1); + SG_CHECK_EQUAL(proxy.getString("/resource1"), s1); + SG_CHECK_EQUAL(rproxy.getString("/resource1"), rs1); + + SG_CHECK_EQUAL(proxy.getStringDecideOnPrefix("/resource2"), rlipsum); + SG_CHECK_EQUAL(proxy.getStringDecideOnPrefix(":/resource2"), lipsum); + SG_CHECK_EQUAL(proxy.getString("/resource2", false), rlipsum); + SG_CHECK_EQUAL(proxy.getString("/resource2", true), lipsum); + SG_CHECK_EQUAL(proxy.getString("/resource2"), lipsum); + SG_CHECK_EQUAL(rproxy.getString("/resource2"), rlipsum); + + // Test ResourceProxy::getIStream() + auxTest_ResourceProxy_getIStream(proxy.getIStreamDecideOnPrefix("/resource1"), + rs1); + auxTest_ResourceProxy_getIStream(proxy.getIStreamDecideOnPrefix(":/resource1"), + s1); + auxTest_ResourceProxy_getIStream(proxy.getIStream("/resource1"), s1); + auxTest_ResourceProxy_getIStream(rproxy.getIStream("/resource1"), rs1); + + auxTest_ResourceProxy_getIStream(proxy.getIStream("/resource2", false), + rlipsum); + auxTest_ResourceProxy_getIStream(proxy.getIStream("/resource2", true), + lipsum); + auxTest_ResourceProxy_getIStream(proxy.getIStream("/resource2"), lipsum); + auxTest_ResourceProxy_getIStream(rproxy.getIStream("/resource2"), rlipsum); +} + int main(int argc, char **argv) { // Initialize the EmbeddedResourceManager instance, add a few resources @@ -407,6 +506,7 @@ int main(int argc, char **argv) test_addAlreadyExistingResource(); test_localeDependencyOfResourceFetching(); test_getLocaleAndSelectLocale(); + test_ResourceProxy(); return EXIT_SUCCESS; }