From e5e112c3c2b5aafa3f179880558abd2011cf08c8 Mon Sep 17 00:00:00 2001 From: Florent Rougon Date: Sat, 12 Aug 2017 11:42:21 +0200 Subject: [PATCH] Add a ResourceProxy class 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 (from FlightGear for the globals->get_fg_root() bit): 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 don't worry about it anymore---it's stored inside the ResourceProxy object. 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"); (alternatively, you could use several ResourceProxy objects with different values for the constructor's third parameter) --- simgear/embedded_resources/CMakeLists.txt | 4 +- simgear/embedded_resources/ResourceProxy.cxx | 247 ++++++++++++++++++ simgear/embedded_resources/ResourceProxy.hxx | 152 +++++++++++ .../embedded_resources_test.cxx | 100 +++++++ 4 files changed, 501 insertions(+), 2 deletions(-) create mode 100644 simgear/embedded_resources/ResourceProxy.cxx create mode 100644 simgear/embedded_resources/ResourceProxy.hxx 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; }