mirror of
https://github.com/copyrighttxt/watrbx-game-engine.git
synced 2026-09-04 20:57:49 +00:00
1064 lines
33 KiB
C++
1064 lines
33 KiB
C++
/* Copyright 2003-2005 ROBLOX Corporation, All Rights Reserved */
|
|
#include "stdafx.h"
|
|
|
|
#include "v8datamodel/ContentProvider.h"
|
|
#include "Util/ScriptInformationProvider.h"
|
|
#include "RbxAssert.h"
|
|
#include "v8xml/serializer.h"
|
|
#include "v8xml/xmlserializer.h"
|
|
#include "Util/http.h"
|
|
#include <string.h>
|
|
#include "Util/StandardOut.h"
|
|
#include "Util/FileSystem.h"
|
|
#include "Util/ThreadPool.h"
|
|
#include "Util/Statistics.h"
|
|
#include "V8DataModel/DataModel.h"
|
|
#include "rbx/Crypt.h"
|
|
#include "StringConv.h"
|
|
#include "RobloxServicesTools.h"
|
|
|
|
#if defined(_WIN32) && !defined(RBX_PLATFORM_DURANGO)
|
|
#include "ATLPath.h"
|
|
#include "FastLog.h"
|
|
#endif
|
|
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <iterator>
|
|
#include <sys/stat.h>
|
|
#include <boost/scoped_ptr.hpp>
|
|
#include <boost/shared_ptr.hpp>
|
|
#include <boost/iostreams/copy.hpp>
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <boost/filesystem.hpp>
|
|
|
|
// Prevent ContentFilter from calling the stats handler
|
|
// TODO: Filter even harder so that ContentFilter *only* calls asset handler
|
|
|
|
#define BOOST_DATE_TIME_NO_LIB
|
|
#include "boost/date_time/posix_time/posix_time.hpp"
|
|
|
|
#define BOOST_FILESYSTEM_NO_DEPRECATED
|
|
#include "boost/filesystem.hpp"
|
|
|
|
#ifdef RBX_TEST_BUILD
|
|
#include "Util/Statistics.h"
|
|
#endif
|
|
|
|
#include "XStudioBuild.h"
|
|
|
|
using namespace boost::posix_time;
|
|
namespace fs = boost::filesystem;
|
|
|
|
LOGGROUP(ContentProviderCleanup)
|
|
LOGGROUP(ContentProviderRequests)
|
|
LOGGROUP(ContentProviderRequestsFullContent)
|
|
FASTFLAGVARIABLE(NoCacheForLocalContent, false)
|
|
DYNAMIC_FASTINTVARIABLE(ContentProviderThreadPoolSize, 16)
|
|
DYNAMIC_FASTFLAGVARIABLE(ContentProviderHttpCaching, false)
|
|
|
|
DYNAMIC_FASTFLAGVARIABLE(ImageFailedToLoadContext, false)
|
|
DYNAMIC_FASTFLAG(UrlReconstructToAssetGame)
|
|
|
|
using namespace boost::posix_time;
|
|
|
|
#ifdef _WIN32
|
|
#define APPLOG(message) do { if (appLog != NULL) {RBX::mutex::scoped_lock lock(*appLogLock); appLog->writeEntry(RBX::Log::Information, message); } } while(0)
|
|
#else
|
|
#define APPLOG(message) {}
|
|
#endif
|
|
|
|
namespace {
|
|
inline std::string pathToString(const fs::path &path)
|
|
{
|
|
#ifdef _WIN32
|
|
return RBX::utf8_encode(path.wstring());
|
|
#else
|
|
return path.string();
|
|
#endif
|
|
}
|
|
|
|
inline fs::path stringToPath(const std::string& str)
|
|
{
|
|
#ifdef _WIN32
|
|
return RBX::utf8_decode(str.c_str());
|
|
#else
|
|
return fs::path(str);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
namespace RBX {
|
|
extern void ThrowLastError(const char* message);
|
|
|
|
const char* const sContentProvider = "ContentProvider";
|
|
|
|
Log *ContentProvider::appLog = NULL;
|
|
RBX::mutex *ContentProvider::appLogLock = NULL;
|
|
boost::mutex ContentProvider::preloadContentBlockingMutex;
|
|
|
|
float ContentProvider::PRIORITY_MFC = 0;
|
|
float ContentProvider::PRIORITY_SCRIPT = 10;
|
|
float ContentProvider::PRIORITY_MESH = 20;
|
|
float ContentProvider::PRIORITY_SOLIDMODEL = 25;
|
|
float ContentProvider::PRIORITY_INSERT = 30;
|
|
float ContentProvider::PRIORITY_CHARACTER = 40;
|
|
float ContentProvider::PRIORITY_ANIMATION = 50;
|
|
float ContentProvider::PRIORITY_TEXTURE = 55;
|
|
float ContentProvider::PRIORITY_DECAL = 60;
|
|
float ContentProvider::PRIORITY_DEFAULT = 70;
|
|
float ContentProvider::PRIORITY_SOUND = 80;
|
|
float ContentProvider::PRIORITY_GUI = 90;
|
|
float ContentProvider::PRIORITY_SKY = 100;
|
|
|
|
REFLECTION_BEGIN();
|
|
static Reflection::BoundFuncDesc<ContentProvider, void(std::string)> func_setBaseUrl( &ContentProvider::setBaseUrl, "SetBaseUrl", "url", Security::LocalUser);
|
|
Reflection::PropDescriptor<ContentProvider, std::string> ContentProvider::desc_baseUrl("BaseUrl", category_Data, &ContentProvider::getBaseUrl, NULL);
|
|
static Reflection::BoundFuncDesc<ContentProvider, void(std::string)> func_setAssetUrl( &ContentProvider::setBaseUrl, "SetAssetUrl", "url", Security::LocalUser);
|
|
static Reflection::BoundFuncDesc<ContentProvider, void(int)> func_setThreadPool( &ContentProvider::setThreadPool, "SetThreadPool", "count", Security::LocalUser);
|
|
static Reflection::BoundFuncDesc<ContentProvider, void(int)> func_setCacheSize( &ContentProvider::setCacheSize, "SetCacheSize", "count", Security::LocalUser);
|
|
static Reflection::BoundFuncDesc<ContentProvider, void(ContentId)> func_preload( &ContentProvider::preloadContent, "Preload", "contentId", Security::None);
|
|
static Reflection::BoundYieldFuncDesc<ContentProvider, void(shared_ptr<const Reflection::ValueArray>)> func_preloadAsync( &ContentProvider::preloadContentBlockingList, "PreloadAsync", "contentIdList", Security::None);
|
|
|
|
static Reflection::PropDescriptor<ContentProvider, int> desc_requestQueueSize("RequestQueueSize", category_Data, &ContentProvider::getRequestQueueSize, NULL);
|
|
REFLECTION_END();
|
|
|
|
const std::string& ContentProvider::getBaseUrl() const
|
|
{
|
|
return baseUrl;
|
|
}
|
|
|
|
const std::string ContentProvider::getApiBaseUrl() const
|
|
{
|
|
return getApiBaseUrl(baseUrl);
|
|
}
|
|
|
|
const std::string ContentProvider::getUnsecureApiBaseUrl() const
|
|
{
|
|
return getUnsecureApiBaseUrl(baseUrl);
|
|
}
|
|
|
|
std::string ContentProvider::getApiBaseUrl(const std::string& baseUrl)
|
|
{
|
|
std::string apiBaseUrl = getUnsecureApiBaseUrl(baseUrl);
|
|
if(!apiBaseUrl.empty())
|
|
{
|
|
std::size_t foundPos = apiBaseUrl.find("https");
|
|
if(foundPos == std::string::npos)
|
|
{
|
|
foundPos = apiBaseUrl.find("http");
|
|
if (foundPos != std::string::npos)
|
|
{
|
|
apiBaseUrl.replace(foundPos,4,"https");
|
|
}
|
|
}
|
|
return apiBaseUrl;
|
|
}
|
|
|
|
return baseUrl;
|
|
}
|
|
|
|
std::string ContentProvider::getUnsecureApiBaseUrl(const std::string& baseUrl)
|
|
{
|
|
if(!baseUrl.empty())
|
|
{
|
|
return ReplaceTopSubdomain(baseUrl, "api");
|
|
}
|
|
|
|
return baseUrl;
|
|
}
|
|
|
|
|
|
void ContentProvider::setBaseUrl(std::string url)
|
|
{
|
|
if (this->baseUrl != url)
|
|
{
|
|
this->baseUrl = url;
|
|
raisePropertyChanged(desc_baseUrl);
|
|
}
|
|
}
|
|
void ContentProvider::setThreadPool(int count)
|
|
{
|
|
contentCache->setThreadPool(count);
|
|
}
|
|
void ContentProvider::setCacheSize(int count)
|
|
{
|
|
contentCache->setCacheSize(count);
|
|
}
|
|
shared_ptr<const Reflection::ValueArray> ContentProvider::getFailedUrls()
|
|
{
|
|
return contentCache->getFailedUrls();
|
|
}
|
|
int ContentProvider::getRequestQueueSize() const
|
|
{
|
|
return contentCache->getRequestQueueSize();
|
|
}
|
|
shared_ptr<const Reflection::ValueArray> ContentProvider::getRequestQueueUrls()
|
|
{
|
|
return contentCache->getRequestQueueUrls();
|
|
}
|
|
shared_ptr<const Reflection::ValueArray> ContentProvider::getRequestedUrls()
|
|
{
|
|
return contentCache->getRequestedUrls();
|
|
}
|
|
|
|
bool ContentProvider::findLocalFile(const std::string& url, std::string* filename)
|
|
{
|
|
ContentId id(url);
|
|
|
|
if (id.isAsset() || id.isAppContent())
|
|
{
|
|
*filename = findAsset(id);
|
|
if (filename->empty())
|
|
return false;
|
|
}
|
|
else if(!id.isHttp())
|
|
{
|
|
//This will only work if contendId is actually a hash
|
|
*filename = findHashFile(id);
|
|
|
|
if(filename->empty()){
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
ContentProvider::ContentProvider()
|
|
: afm(NULL)
|
|
{
|
|
setName(sContentProvider);
|
|
|
|
contentCache.reset(new AsyncHttpCache<CachedContent>(this,&findLocalFile, DFInt::ContentProviderThreadPoolSize, 1000));
|
|
|
|
if (DFFlag::ContentProviderHttpCaching)
|
|
{
|
|
contentCache->setCachePolicy(HttpCache::PolicyFinalRedirect);
|
|
}
|
|
|
|
}
|
|
|
|
ContentProvider::~ContentProvider()
|
|
{
|
|
RBX::FileSystem::clearCacheDirectory("ContentProvider");
|
|
}
|
|
|
|
void ContentProvider::verifyRequestedScriptSignature(const ProtectedString& source, const std::string& assetId, bool required)
|
|
{
|
|
const char* script = source.getSource().c_str();
|
|
|
|
try
|
|
{
|
|
verifyScriptSignature(source, required);
|
|
|
|
// search for the asset header
|
|
// looks like "--rbxassetid%1818%"
|
|
const char* assetHeader = "--rbxassetid%";
|
|
const char* idInScript = strstr(script, assetHeader); // will find first occurrence, which should be safe
|
|
if (idInScript)
|
|
{
|
|
idInScript += strlen(assetHeader); // now points to asset id in script, terminated by another %
|
|
if (strncmp(idInScript, assetId.c_str(), assetId.length()) == 0 && // ensure match
|
|
idInScript[assetId.length()] == '%') // terminated here
|
|
{
|
|
return; // correct
|
|
}
|
|
else
|
|
{
|
|
throw std::runtime_error(""); // mismatch
|
|
}
|
|
}
|
|
|
|
if (required)
|
|
{
|
|
throw std::runtime_error("");
|
|
}
|
|
}
|
|
catch (RBX::base_exception&)
|
|
{
|
|
//Intentionally strip out the exception call stack location
|
|
throw std::runtime_error("");
|
|
}
|
|
}
|
|
|
|
void ContentProvider::verifyScriptSignature(const ProtectedString& source, bool required)
|
|
{
|
|
const char* script = source.getSource().c_str();
|
|
|
|
try
|
|
{
|
|
// sig can be behind a Lua comment
|
|
// looks like "--rbxsig%MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCtfLLFT36v5r9bNP7STBteDU5a%"
|
|
const char* sigHeader = "--rbxsig%";
|
|
if (strncmp(script, sigHeader, strlen(sigHeader)) == 0)
|
|
{
|
|
const char* sigStart = script + strlen(sigHeader);
|
|
const char* sigEnd = strchr(sigStart, '%');
|
|
if (!sigEnd)
|
|
{
|
|
throw std::runtime_error("");
|
|
}
|
|
std::string signature(sigStart, sigEnd - sigStart);
|
|
const char* signedScript = sigEnd + 1; // skip terminal %. we signed the text after this signature.
|
|
|
|
// verify now!, will throw runtime_error
|
|
Crypt().verifySignatureBase64(signedScript, signature);
|
|
|
|
return;
|
|
}
|
|
|
|
if (required)
|
|
{
|
|
throw std::runtime_error("");
|
|
}
|
|
}
|
|
catch (RBX::base_exception&)
|
|
{
|
|
//Intentionally strip out the exceptions
|
|
throw std::runtime_error("");
|
|
}
|
|
}
|
|
|
|
void ContentProvider::onHeartbeat(const Heartbeat& heartbeatEvent)
|
|
{
|
|
contentCache->onHeartbeat(heartbeatEvent);
|
|
}
|
|
|
|
void ContentProvider::setAssetFetchMediator(AssetFetchMediator* afm)
|
|
{
|
|
this->afm = afm;
|
|
}
|
|
|
|
std::string ContentProvider::getAssetFile(const char* filePath) {
|
|
return std::string(assetFolder() + filePath);
|
|
}
|
|
|
|
bool ContentProvider::hasContent(const RBX::ContentId& id) {
|
|
return isContentLoaded(id);
|
|
}
|
|
|
|
bool ContentProvider::isUrlBad(RBX::ContentId id) {
|
|
id.convertAssetId(baseUrl, DataModel::get(this)->getUniverseId());
|
|
id.convertToLegacyContent(baseUrl);
|
|
return contentCache->isUrlBad(id.toString());
|
|
}
|
|
|
|
bool ContentProvider::isRequestQueueEmpty()
|
|
{
|
|
return contentCache->isRequestQueueEmpty();
|
|
}
|
|
|
|
bool ContentProvider::registerFile(const ContentId& id, CachedContent* item)
|
|
{
|
|
if (!item->filename)
|
|
{
|
|
std::istringstream ss(*item->data);
|
|
ContentId hash = registerContent(ss);
|
|
std::string filename = findHashFile(hash);
|
|
if (filename.size()==0)
|
|
return false;
|
|
|
|
item->filename.reset(new std::string(filename));
|
|
|
|
contentCache->insertCacheItem(id.c_str(),*item);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void returnResponse(AsyncHttpQueue::RequestResult result, boost::function<void (AsyncHttpQueue::RequestResult)> callback)
|
|
{
|
|
callback(result);
|
|
}
|
|
|
|
static void parseString(
|
|
const RBX::ContentId& id,
|
|
AsyncHttpQueue::RequestResult result,
|
|
std::istream* stream,
|
|
boost::function<void (AsyncHttpQueue::RequestResult, shared_ptr<const std::string>, shared_ptr<std::exception>)> callback,
|
|
shared_ptr<const std::string> response,
|
|
shared_ptr<std::exception> requestError)
|
|
{
|
|
shared_ptr<std::string> returnedResponse(new std::string());
|
|
if (result==AsyncHttpQueue::Succeeded)
|
|
{
|
|
|
|
try{
|
|
FASTLOGS(FLog::ContentProviderRequests, "Got content as string: %s", id.c_str());
|
|
|
|
std::ostringstream strbuffer;
|
|
strbuffer << stream->rdbuf();
|
|
*returnedResponse = strbuffer.str();
|
|
}
|
|
catch(std::runtime_error e)
|
|
{
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "Content failed to load for %s because %s", id.c_str(), e.what());
|
|
result = AsyncHttpQueue::Failed;
|
|
}
|
|
}
|
|
|
|
callback(result, returnedResponse, requestError);
|
|
}
|
|
|
|
static void parseContent(
|
|
const RBX::ContentId& id,
|
|
AsyncHttpQueue::RequestResult result,
|
|
std::istream* stream,
|
|
boost::function<void (AsyncHttpQueue::RequestResult, shared_ptr<Instances>, shared_ptr<std::exception>)> callback,
|
|
shared_ptr<const std::string> response,
|
|
shared_ptr<std::exception> requestError)
|
|
{
|
|
shared_ptr<Instances> instances(new Instances());
|
|
|
|
if (result==AsyncHttpQueue::Succeeded)
|
|
{
|
|
try
|
|
{
|
|
FASTLOGS(FLog::ContentProviderRequests, "Parsing content: %s", id.c_str());
|
|
|
|
Serializer().loadInstances(*stream, *instances);
|
|
}
|
|
catch(std::runtime_error e)
|
|
{
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "Content failed to parse for %s because %s", id.c_str(), e.what());
|
|
result = AsyncHttpQueue::Failed;
|
|
}
|
|
}
|
|
|
|
callback(result, instances, requestError);
|
|
}
|
|
|
|
void ContentProvider::preloadContentWithCallback(RBX::ContentId id, float priority, boost::function<void (AsyncHttpQueue::RequestResult)> callback, AsyncHttpQueue::ResultJob jobType, const std::string& expectedType)
|
|
{
|
|
getContent(id, priority, boost::bind(&returnResponse, _1, callback), jobType, expectedType);
|
|
}
|
|
|
|
void ContentProvider::preloadContentResultCallback(AsyncHttpQueue::RequestResult results, ContentId id)
|
|
{
|
|
if (results != AsyncHttpQueue::Succeeded)
|
|
{
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "ContentProvider:Preload() failed for %s", id.c_str());
|
|
}
|
|
}
|
|
|
|
void ContentProvider::preloadContent(RBX::ContentId id)
|
|
{
|
|
if (DFFlag::ImageFailedToLoadContext)
|
|
{
|
|
AsyncHttpQueue::RequestCallback callback = boost::bind(&ContentProvider::preloadContentResultCallback, this, _1, id);
|
|
AsyncHttpQueue::RequestResult requestResult = privateLoadContent(id, AsyncHttpRequest, INT_MAX, NULL, &callback);
|
|
if (requestResult != AsyncHttpQueue::Waiting)
|
|
preloadContentResultCallback(requestResult, id);
|
|
}
|
|
else
|
|
{
|
|
privateLoadContent(id, AsyncHttpRequest, INT_MAX, NULL, NULL);
|
|
}
|
|
}
|
|
|
|
void ContentProvider::preloadContentBlockingListHelper(AsyncHttpQueue::RequestResult results, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction, PreloadAsyncRequest *pRequest, ContentId id)
|
|
{
|
|
boost::mutex::scoped_lock lock(preloadContentBlockingMutex);
|
|
|
|
pRequest->outstanding--;
|
|
if (results != AsyncHttpQueue::Succeeded)
|
|
{
|
|
if (DFFlag::ImageFailedToLoadContext)
|
|
{
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "ContentProvider:PreloadAsync() failed for %s", id.c_str());
|
|
}
|
|
pRequest->failed++;
|
|
}
|
|
|
|
if (pRequest->outstanding == 0)
|
|
{
|
|
resumeFunction();
|
|
delete pRequest;
|
|
}
|
|
}
|
|
|
|
void ContentProvider::preloadContentBlockingList(shared_ptr<const Reflection::ValueArray> idList, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)
|
|
{
|
|
const Reflection::ValueArray *pList = idList.get();
|
|
|
|
if (pList && pList->size() > 0)
|
|
{
|
|
for (size_t i = 0; i < pList->size(); i++)
|
|
{
|
|
Reflection::Variant v = (*pList)[i];
|
|
ContentId id = v.get<RBX::ContentId>();
|
|
|
|
if (!id.isNull() && !id.isAssetId() && !id.isHttp())
|
|
{
|
|
errorFunction("PreloadAsync: Bad format of asset " + v.get<std::string>());
|
|
return;
|
|
}
|
|
}
|
|
|
|
PreloadAsyncRequest *pRequest = new PreloadAsyncRequest(pList->size());
|
|
for (size_t i = 0; i < pList->size(); i++)
|
|
{
|
|
Reflection::Variant v = (*pList)[i];
|
|
ContentId id = v.get<RBX::ContentId>();
|
|
AsyncHttpQueue::RequestCallback callback = boost::bind(&ContentProvider::preloadContentBlockingListHelper, this, _1, resumeFunction, errorFunction, pRequest, id);
|
|
AsyncHttpQueue::RequestResult requestResult = privateLoadContent(id, AsyncHttpRequest, INT_MAX, NULL, &callback);
|
|
if (requestResult != AsyncHttpQueue::Waiting)
|
|
preloadContentBlockingListHelper(requestResult, resumeFunction, errorFunction, pRequest, id);
|
|
}
|
|
}
|
|
else {
|
|
resumeFunction();
|
|
}
|
|
}
|
|
|
|
void ContentProvider::clearContent()
|
|
{
|
|
contentCache->clearCache();
|
|
}
|
|
|
|
void ContentProvider::invalidateCache(ContentId id)
|
|
{
|
|
id.convertAssetId(baseUrl, DataModel::get(this)->getUniverseId());
|
|
id.convertToLegacyContent(baseUrl);
|
|
contentCache->invalidateCacheItemOrFailure(id.toString());
|
|
}
|
|
|
|
void ContentProvider::loadContent(
|
|
const RBX::ContentId& id,
|
|
float priority,
|
|
boost::function<void (AsyncHttpQueue::RequestResult, shared_ptr<Instances>, shared_ptr<std::exception>)> callback,
|
|
AsyncHttpQueue::ResultJob jobType)
|
|
{
|
|
getContent(id, priority, boost::bind(&parseContent, id, _1, _2, callback, _3, _4), jobType);
|
|
}
|
|
|
|
void ContentProvider::loadContentString(
|
|
const RBX::ContentId& id,
|
|
float priority,
|
|
boost::function<void (AsyncHttpQueue::RequestResult, shared_ptr<const std::string>, shared_ptr<std::exception>)> callback,
|
|
AsyncHttpQueue::ResultJob jobType)
|
|
{
|
|
getContent(id, priority, boost::bind(&parseString, id, _1, _2, callback, _3, _4), jobType);
|
|
}
|
|
|
|
static void InvokeFileCallback(AsyncHttpQueue::RequestCallback callback, boost::shared_ptr<const std::string> filename)
|
|
{
|
|
std::ifstream fileStream(utf8_decode(*filename).c_str(), std::ios_base::in | std::ios_base::binary);
|
|
shared_ptr<std::exception> nullException;
|
|
callback(AsyncHttpQueue::Succeeded, &fileStream, shared_ptr<const std::string>(), nullException);
|
|
}
|
|
|
|
void ContentProvider::getContent(
|
|
const RBX::ContentId& id,
|
|
float priority,
|
|
AsyncHttpQueue::RequestCallback callback,
|
|
AsyncHttpQueue::ResultJob jobType,
|
|
const std::string &expectedType)
|
|
{
|
|
ContentId activeId = id;
|
|
RBXASSERT(this!=NULL);
|
|
|
|
CachedContent item;
|
|
|
|
AsyncHttpQueue::RequestResult requestResult = privateLoadContent(activeId, AsyncHttpRequest, priority, &item, &callback, jobType, expectedType);
|
|
switch(requestResult){
|
|
case AsyncHttpQueue::Succeeded:
|
|
if(item.data){
|
|
shared_ptr<std::exception> nullException;
|
|
AsyncHttpQueue::dispatchCallback(callback, this, AsyncHttpQueue::Succeeded,item.data, jobType, nullException);
|
|
}
|
|
else{
|
|
AsyncHttpQueue::dispatchGenericCallback(boost::bind(&InvokeFileCallback, callback, item.filename), this, jobType);
|
|
}
|
|
break;
|
|
case AsyncHttpQueue::Failed:
|
|
AsyncHttpQueue::dispatchCallback(callback, this, AsyncHttpQueue::Failed, boost::shared_ptr<const std::string>(new std::string("")), jobType, shared_ptr<std::exception>(new std::runtime_error("Temp read failed.")));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
shared_ptr<const std::string> ContentProvider::getContentString(RBX::ContentId id)
|
|
{
|
|
APPLOG("ContentProvider::getContentString");
|
|
|
|
shared_ptr<const std::string> result = requestContentString(id, ContentProvider::PRIORITY_SCRIPT);
|
|
if (!result)
|
|
{
|
|
CachedContent item;
|
|
if (!blockingLoadContent(id, &item))
|
|
throw RBX::runtime_error("Unable to load %s", id.c_str());
|
|
|
|
result = requestContentString(id, ContentProvider::PRIORITY_SCRIPT);
|
|
if (!result)
|
|
throw RBX::runtime_error("Unable to retrieve cache of %s", id.c_str());
|
|
}
|
|
RBXASSERT(result);
|
|
return result;
|
|
}
|
|
|
|
shared_ptr<const std::string> ContentProvider::requestContentString(const RBX::ContentId& id, float priority)
|
|
{
|
|
RBXASSERT(this!=NULL);
|
|
APPLOG("ContentProvider::requestContentString - enter");
|
|
RBX::ContentId activeId = id;
|
|
|
|
CachedContent item;
|
|
if (AsyncHttpQueue::Succeeded != privateLoadContent(activeId, AsyncHttpRequest, priority, &item, NULL))
|
|
return shared_ptr<const std::string>();
|
|
|
|
if (!item.data)
|
|
{
|
|
APPLOG("ContentProvider::requestContentString - putting data into cache");
|
|
|
|
// Convert file to string data
|
|
|
|
std::ifstream stream(utf8_decode(*item.filename).c_str(), std::ios_base::in | std::ios_base::binary);
|
|
std::ostringstream data;
|
|
|
|
#if (defined(_DEBUG) && defined(__APPLE__))
|
|
#include "TargetConditionals.h"
|
|
#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
|
|
data << stream.rdbuf(); // iOS in debug does not like boost copy here for some reason
|
|
#else
|
|
boost::iostreams::copy(stream,data);
|
|
#endif
|
|
#else
|
|
boost::iostreams::copy(stream,data);
|
|
#endif
|
|
|
|
item.data.reset(new std::string(data.str()));
|
|
|
|
if(!FFlag::NoCacheForLocalContent)
|
|
contentCache->insertCacheItem( activeId.c_str(), item );
|
|
}
|
|
APPLOG("ContentProvider::requestContentString - leave");
|
|
return item.data;
|
|
}
|
|
|
|
bool ContentProvider::blockingLoadContent(ContentId id, CachedContent* result, const std::string& expectedType)
|
|
{
|
|
APPLOG(RBX::format("ContentProvider::requestContentString %s", id.c_str()).c_str());
|
|
return privateLoadContent(id, SyncHttpRequest, -1, result, NULL, AsyncHttpQueue::AsyncInline, expectedType) == AsyncHttpQueue::Succeeded;
|
|
}
|
|
bool ContentProvider::isContentLoaded(ContentId id)
|
|
{
|
|
return privateLoadContent(id, NoHttpRequest, -1, NULL, NULL) == AsyncHttpQueue::Succeeded;
|
|
}
|
|
|
|
AsyncHttpQueue::RequestResult ContentProvider::privateLoadContent(RBX::ContentId& id, RequestType httpRequestType, float priority, CachedContent* result, AsyncHttpQueue::RequestCallback* callback, AsyncHttpQueue::ResultJob jobType, const std::string& expectedType)
|
|
{
|
|
APPLOG("ContentProvider::privateLoadContent");
|
|
|
|
if (id.isNull())
|
|
return AsyncHttpQueue::Failed;
|
|
|
|
FASTLOGS(FLog::ContentProviderRequests, "Content requested: %s", id.c_str());
|
|
|
|
if (afm)
|
|
{
|
|
if (boost::optional<std::string> content = afm->findCachedAssetOrEmpty(id, DataModel::get(this)->getUniverseId()))
|
|
{
|
|
*result = CachedContent(shared_ptr<std::string>(new std::string(content.get())), shared_ptr<std::string>());
|
|
return AsyncHttpQueue::Succeeded;
|
|
}
|
|
}
|
|
|
|
id.convertAssetId(baseUrl, DataModel::get(this)->getUniverseId());
|
|
id.convertToLegacyContent(baseUrl);
|
|
|
|
if (!id.reconstructAssetUrl(baseUrl))
|
|
return AsyncHttpQueue::Failed;
|
|
|
|
if (contentCache->findCacheItem(id.toString(), result))
|
|
{
|
|
APPLOG("ContentProvider::privateLoadContent - found item in cache");
|
|
return AsyncHttpQueue::Succeeded;
|
|
}
|
|
|
|
if( httpRequestType != FullAsyncRequest )
|
|
{
|
|
APPLOG("ContentProvider::privateLoadContent - httpRequestType != FullAsyncRequest");
|
|
if (id.isAsset())
|
|
{
|
|
|
|
shared_ptr<const std::string> filename(new std::string(findAsset(id)));
|
|
if (filename->empty())
|
|
return AsyncHttpQueue::Failed;
|
|
contentCache->insertCacheItem(id.toString(), CachedContent(filename));
|
|
return contentCache->findCacheItem(id.toString(), result) ? AsyncHttpQueue::Succeeded : AsyncHttpQueue::Waiting;
|
|
}
|
|
|
|
#ifdef RBX_TEST_BUILD
|
|
if (id.isFile())
|
|
{
|
|
const std::string cfilename = id.toString().substr(7);
|
|
boost::filesystem::path path;
|
|
|
|
if (boost::filesystem::path(cfilename).is_absolute())
|
|
{
|
|
path = cfilename;
|
|
}
|
|
else
|
|
{
|
|
boost::filesystem::path root = GetDefaultFilePath();
|
|
boost::filesystem::path path = root / cfilename;
|
|
|
|
struct stat buffer;
|
|
// Try once to find the path. If it fails, try once more with the
|
|
// parent directory. The logic for this is that RobloxStudio
|
|
// will look for scripts relative to the RBXL, but RobloxTest
|
|
// will look for scripts relative to the ProjectDir for RobloxTest,
|
|
// which is one higher than the RBXL. So, in the case of
|
|
// RobloxStudio, we forcefully push one directory higher if the file
|
|
// wasn't found next to the RBXL.
|
|
if (-1 == stat(path.string().c_str(), &buffer))
|
|
{
|
|
path = root / ".." / cfilename;
|
|
}
|
|
}
|
|
|
|
shared_ptr<const std::string> filename(new std::string(path.string()));
|
|
contentCache->insertCacheItem(id.toString(), CachedContent(filename));
|
|
return contentCache->findCacheItem(id.toString(), result) ? AsyncHttpQueue::Succeeded : AsyncHttpQueue::Waiting;
|
|
}
|
|
#endif
|
|
|
|
// If content is not http, try seeing if it's hashed on disk (according to Erik, for legacy embedded content feature)
|
|
if (!id.isHttp())
|
|
{
|
|
//This will only work if contendId is actually a hash
|
|
shared_ptr<const std::string> filename(new std::string(findHashFile(id)));
|
|
if (!filename->empty())
|
|
{
|
|
contentCache->insertCacheItem(id.toString(), CachedContent(filename));
|
|
return contentCache->findCacheItem(id.toString(), result) ? AsyncHttpQueue::Succeeded : AsyncHttpQueue::Waiting;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we get all the way here then try to request it for later use
|
|
switch (httpRequestType)
|
|
{
|
|
case NoHttpRequest:
|
|
break;
|
|
case AsyncHttpRequest:
|
|
if (!id.isHttp()){
|
|
break;
|
|
}
|
|
//Fallthrough
|
|
case FullAsyncRequest:
|
|
{
|
|
FASTLOGS(FLog::ContentProviderRequests, "Content requested: %s", id.c_str());
|
|
APPLOG("ContentProvider::privateLoadContent - requestType = FullAsyncRequest");
|
|
contentCache->asyncRequest(id.c_str(), priority, callback, jobType, false, expectedType);
|
|
return AsyncHttpQueue::Waiting;
|
|
}
|
|
|
|
case SyncHttpRequest:
|
|
{
|
|
APPLOG("ContentProvider::privateLoadContent - requestType = SyncHttpRequest");
|
|
if(contentCache->syncRequest(id.c_str(), expectedType)){
|
|
return contentCache->findCacheItem(id.c_str(), result) ? AsyncHttpQueue::Succeeded : AsyncHttpQueue::Failed;
|
|
}
|
|
return AsyncHttpQueue::Waiting;
|
|
}
|
|
}
|
|
|
|
return AsyncHttpQueue::Failed;
|
|
}
|
|
|
|
bool ContentProvider::isHttpUrl(const std::string& s)
|
|
{
|
|
if (s.find("http://")==0)
|
|
return true;
|
|
if (s.find("https://")==0)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
bool ContentProvider::assetFolderAlreadyInit = false;
|
|
fs::path ContentProvider::assetFolderPath;
|
|
fs::path ContentProvider::platformAssetFolderPath;
|
|
std::string ContentProvider::assetFolderString;
|
|
std::string ContentProvider::platformAssetFolderString;
|
|
|
|
std::string ContentProvider::assetFolder()
|
|
{
|
|
return assetFolderString;
|
|
}
|
|
|
|
std::string ContentProvider::platformAssetFolder()
|
|
{
|
|
return platformAssetFolderString;
|
|
}
|
|
|
|
bool ContentProvider::isUrl(const std::string& s)
|
|
{
|
|
if (isHttpUrl(s))
|
|
return true;
|
|
if (s.find("rbxasset://")==0 && s.length() > std::string("rbxasset://").length())
|
|
return true;
|
|
if (s.find("rbxassetid://")==0 && s.length() > std::string("rbxassetid://").length())
|
|
return true;
|
|
if (ContentId(s).isNamedAsset())
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
void ContentProvider::blockingLoadInstances(ContentId id, std::vector<shared_ptr<Instance> >& instances)
|
|
{
|
|
boost::shared_ptr<const std::string> str = getContentString(id);
|
|
|
|
std::istringstream stream(*str);
|
|
|
|
Serializer().loadInstances(stream, instances);
|
|
}
|
|
|
|
std::string ContentProvider::getFile(RBX::ContentId ticket)
|
|
{
|
|
CachedContent item;
|
|
if (!blockingLoadContent(ticket, &item) || !registerFile(ticket, &item))
|
|
throw RBX::runtime_error("Unable to load %s", ticket.c_str());
|
|
|
|
return *item.filename;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include "Util/MD5Hasher.h"
|
|
|
|
namespace RBX
|
|
{
|
|
static fs::path getCachePath(const char* subFolder, bool createPath)
|
|
{
|
|
// Returns something like "C:\Documents and Settings\All Users\Application Data\Roblox\Cache"
|
|
if (createPath)
|
|
{
|
|
static const fs::path path = RBX::FileSystem::getCacheDirectory(true, subFolder);
|
|
return path;
|
|
}
|
|
else
|
|
{
|
|
static const fs::path path = RBX::FileSystem::getCacheDirectory(false, subFolder);
|
|
return path;
|
|
}
|
|
}
|
|
|
|
|
|
static void appendSlashIfRequired(fs::path& path)
|
|
{
|
|
std::string pathStr = path.string();
|
|
if ( pathStr[pathStr.length() - 1] != '/' && pathStr[pathStr.length() - 1] != '\\')
|
|
path /= "/";
|
|
}
|
|
|
|
static fs::path getLocalCachePath(bool createPath)
|
|
{
|
|
static rbx::atomic<int> caching;
|
|
static bool isCached = false;
|
|
static std::string cachedResult;
|
|
|
|
if (isCached)
|
|
return fs::path(cachedResult.c_str());
|
|
|
|
fs::path path = getCachePath("ContentProvider", createPath);
|
|
boost::system::error_code ec;
|
|
if (!boost::filesystem::exists(path, ec))
|
|
return "";
|
|
|
|
appendSlashIfRequired(path);
|
|
|
|
if (caching.compare_and_swap(1, 0) == 0)
|
|
{
|
|
cachedResult = std::string(path.string());
|
|
isCached = true;
|
|
}
|
|
return path;
|
|
}
|
|
|
|
RBX::ContentId ContentProvider::registerContent(std::istream& content)
|
|
{
|
|
RBX::ContentId contentId;
|
|
{
|
|
// Get the hash
|
|
boost::scoped_ptr<MD5Hasher> hasher(MD5Hasher::create());
|
|
hasher->addData(content);
|
|
contentId = RBX::ContentId(hasher->toString());
|
|
}
|
|
|
|
// Get/Create the local cache folder
|
|
fs::path filePath = getLocalCachePath(true);
|
|
filePath /= contentId.c_str();
|
|
boost::system::error_code ec;
|
|
if (!boost::filesystem::exists(filePath, ec))
|
|
{
|
|
// TODO: Threading risk here!!!!
|
|
|
|
// Save it to the store
|
|
std::ofstream outStream(filePath.string().c_str(), std::ios_base::out | std::ios::binary);
|
|
|
|
content.clear();
|
|
content.seekg( 0, std::ios_base::beg );
|
|
do
|
|
{
|
|
char buffer[1024];
|
|
content.read(buffer, 1024);
|
|
outStream.write(buffer, content.gcount());
|
|
}
|
|
while (content.gcount()>0);
|
|
}
|
|
|
|
return contentId;
|
|
}
|
|
|
|
bool ContentProvider::isInSandbox(const fs::path& path, const fs::path& sandbox)
|
|
{
|
|
boost::system::error_code errBase, errPath;
|
|
std::string canonicalPath = pathToString(fs::canonical(path,errPath));
|
|
std::string canonicalBase = pathToString(fs::canonical(sandbox,errBase));
|
|
if (errBase || errPath)
|
|
{
|
|
return false;
|
|
}
|
|
else if (canonicalPath.compare(0, canonicalBase.size(), canonicalBase) != 0)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::string ContentProvider::findAsset(RBX::ContentId contentId)
|
|
{
|
|
RBXASSERT(contentId.isAsset() || contentId.isAppContent());
|
|
const char* pathName = contentId.c_str();
|
|
|
|
bool isPlatformAsset = true;
|
|
fs::path queryName(pathName);
|
|
fs::path filePath = platformAssetFolderPath;
|
|
|
|
if (contentId.isAsset())
|
|
{
|
|
pathName += 11;
|
|
filePath /= pathName;
|
|
}
|
|
else if (contentId.isAppContent())
|
|
{
|
|
pathName += 9;
|
|
if ( strcmp(pathName, "xbox/localgamerpic") == 0 )
|
|
{
|
|
filePath = RBX::FileSystem::getUserDirectory(true, DirAppData, "");
|
|
filePath /= "gamerpic.png";
|
|
}
|
|
}
|
|
|
|
|
|
boost::system::error_code ec;
|
|
if (!boost::filesystem::exists(filePath, ec))
|
|
{
|
|
isPlatformAsset = false;
|
|
filePath = assetFolderPath / pathName;
|
|
if (!boost::filesystem::exists(filePath, ec))
|
|
{
|
|
return "";
|
|
}
|
|
}
|
|
|
|
// sandbox to assetFolder() or platformAssetFolder()
|
|
if (contentId.isAppContent() || isInSandbox(filePath, (isPlatformAsset ? platformAssetFolderPath : assetFolderPath)))
|
|
{
|
|
return pathToString(filePath);
|
|
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
std::string ContentProvider::findHashFile(RBX::ContentId contentId)
|
|
{
|
|
// TODO: Confirm that this is a valid "hash" ID
|
|
|
|
// Get/Create the local cache folder
|
|
fs::path cachePath = getLocalCachePath(true);
|
|
// TODO: Handle case where cachePath==""
|
|
|
|
fs::path filePath = cachePath;
|
|
filePath /= contentId.c_str();
|
|
|
|
if (!isInSandbox(filePath, cachePath))
|
|
{
|
|
return "";
|
|
}
|
|
|
|
boost::system::error_code ec;
|
|
if (!boost::filesystem::exists(filePath, ec))
|
|
return "";
|
|
|
|
return pathToString(filePath);
|
|
}
|
|
|
|
void ContentProvider::setAssetFolder(const char* sPath)
|
|
{
|
|
if (!assetFolderAlreadyInit)
|
|
{
|
|
if (RBX::Log::current())
|
|
RBX::Log::current()->writeEntry(Log::Information, RBX::format("setAssetFolder %s", sPath).c_str());
|
|
|
|
fs::path path = fs::system_complete( stringToPath(sPath) );
|
|
|
|
if (!fs::exists(path))
|
|
throw RBX::runtime_error("The path '%s' does not exist", path.string().c_str());
|
|
if (!is_directory(path))
|
|
throw RBX::runtime_error("'%s' is not a directory", path.string().c_str());
|
|
|
|
appendSlashIfRequired(path);
|
|
|
|
#if defined(RBX_PLATFORM_DURANGO)
|
|
fs::path platformAssetFolderModifier = "../PlatformContent/durango/";
|
|
#elif defined(RBX_PLATFORM_IOS)
|
|
fs::path platformAssetFolderModifier = "../ios/";
|
|
#elif defined(__APPLE__) || defined(_WIN32)
|
|
fs::path platformAssetFolderModifier = "../PlatformContent/pc/";
|
|
# if ENABLE_XBOX_STUDIO_BUILD
|
|
platformAssetFolderModifier = "../PlatformContent/Durango/";
|
|
# endif
|
|
#elif defined(__ANDROID__)
|
|
fs::path platformAssetFolderModifier = "../android/";
|
|
#else
|
|
#error Unsupported Platform
|
|
#endif
|
|
|
|
assetFolderString = pathToString(path);
|
|
platformAssetFolderString = assetFolderString + platformAssetFolderModifier.string();
|
|
|
|
assetFolderPath = fs::canonical(path);
|
|
platformAssetFolderPath = fs::canonical(path / platformAssetFolderModifier);
|
|
assetFolderAlreadyInit = true;
|
|
}
|
|
}
|
|
|
|
std::auto_ptr<std::istream> ContentProvider::getContent(const RBX::ContentId& ticket, const std::string& expectedType)
|
|
{
|
|
APPLOG("ContentProvider::getContent - ticket");
|
|
|
|
CachedContent item;
|
|
if (!blockingLoadContent(ticket, &item, expectedType))
|
|
throw RBX::runtime_error("Unable to load %s", ticket.c_str());
|
|
|
|
if (item.data)
|
|
{
|
|
return std::auto_ptr<std::istream>(new std::istringstream(*item.data));
|
|
}
|
|
else
|
|
{
|
|
std::ifstream* stream = new std::ifstream(utf8_decode(*item.filename).c_str(), std::ios_base::in | std::ios_base::binary);
|
|
return std::auto_ptr<std::istream>(stream);
|
|
}
|
|
}
|
|
}
|