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<std::istream> 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)
This commit is contained in:
Florent Rougon
2017-08-12 11:42:21 +02:00
parent dd3cdf63e6
commit e5e112c3c2
4 changed files with 501 additions and 2 deletions

View File

@@ -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}")

View File

@@ -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 <simgear_config.h>
#include <algorithm> // std::find()
#include <ios> // std::streamsize
#include <istream>
#include <limits> // std::numeric_limits
#include <memory>
#include <vector>
#include <string>
#include <cstdlib> // std::size_t
#include <cassert>
#include <simgear/misc/sg_path.hxx>
#include <simgear/misc/strutils.hxx>
#include <simgear/io/iostreams/sgstream.hxx>
#include <simgear/structure/exception.hxx>
#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<string> 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<std::istream>
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<std::istream>(new sg_ifstream(sgPath));
}
}
unique_ptr<std::istream>
ResourceProxy::getIStream(const string& path) const
{
return getIStream(path, _useEmbeddedResourcesByDefault);
}
unique_ptr<std::istream>
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<std::istream> streamp = getIStream(path,
fromEmbeddedResource);
std::streamsize nbCharsRead;
// Allocate a buffer
static constexpr std::size_t bufSize = 65536;
static_assert(bufSize <= std::numeric_limits<std::streamsize>::max(),
"Type std::streamsize is unexpectedly small");
static_assert(bufSize <= std::numeric_limits<string::size_type>::max(),
"Type std::string::size_type is unexpectedly small");
unique_ptr<char[]> 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

View File

@@ -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 <istream>
#include <memory>
#include <string>
#include <simgear/misc/sg_path.hxx>
// 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<std::istream> 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<std::istream> 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<std::istream>
getIStream(const std::string& path, bool fromEmbeddedResource) const;
std::unique_ptr<std::istream>
getIStream(const std::string& path) const;
std::unique_ptr<std::istream>
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

View File

@@ -33,11 +33,14 @@
#include <cstddef> // std::size_t
#include <simgear/misc/test_macros.hxx>
#include <simgear/misc/sg_dir.hxx>
#include <simgear/structure/exception.hxx>
#include <simgear/io/iostreams/CharArrayStream.hxx>
#include <simgear/io/iostreams/sgstream.hxx>
#include <simgear/io/iostreams/zlibstream.hxx>
#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<std::istream> iStream,
const string& contents)
{
cout << "Testing ResourceProxy::getIStream()" << endl;
iStream->exceptions(std::ios_base::badbit);
static constexpr std::size_t bufSize = 65536;
unique_ptr<char[]> 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;
}