0ad/source/ps/scripting/JSInterface_VFS.cpp
wraitii ee0d204bf6 Wrap JSAutoRequest and replace usage of JSContext* with the wrapper.
JSAutoRequest is required before calling into most JSAPI methods, for GC
reasons.
Calling it is required and fragile as one must not forget.
Further, SM52 and later make manipulating JSContext* dangerous as that
can cross Compartment(Realm in SM68) barriers (and ScriptInterface now
matches a Compartment).

The solution to both problems is to avoid using JSContext* in 0 A.D.
itself. To achieve this, a Request class is introduced, and must be used
to access a JSContext* from a scriptInterface. Further, Request is
passed to other ScriptInterface functions isntead of JSContext*, making
it obvious that the caller has already called it, reducing errors and
redundant JSAutoRequest calls.
Only JSNative functions now get a naked JSContext* without protection,
but the likelihood of forgetting a request is lower since many
ScriptInterface functions now expect it.

JSContext* is directly passed to JSAPI functions only.

Part of the SM52 migration, stage: SM45 compatible

Based on a patch by: Itms
Tested By: Freagarach
Refs #4893

Differential Revision: https://code.wildfiregames.com/D3088
This was SVN commit r24176.
2020-11-13 13:18:22 +00:00

275 lines
10 KiB
C++

/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "JSInterface_VFS.h"
#include "lib/file/vfs/vfs_util.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include "ps/Filesystem.h"
#include "scriptinterface/ScriptInterface.h"
#include <sstream>
// Only allow engine compartments to read files they may be concerned about.
#define PathRestriction_GUI {L""}
#define PathRestriction_Simulation {L"simulation/"}
#define PathRestriction_Maps {L"simulation/", L"maps/"}
// shared error handling code
#define JS_CHECK_FILE_ERR(err)\
/* this is liable to happen often, so don't complain */\
if (err == ERR::VFS_FILE_NOT_FOUND)\
{\
return 0; \
}\
/* unknown failure. We output an error message. */\
else if (err < 0)\
LOGERROR("Unknown failure in VFS %i", err );
/* else: success */
// state held across multiple BuildDirEntListCB calls; init by BuildDirEntList.
struct BuildDirEntListState
{
ScriptInterface* pScriptInterface;
JS::PersistentRootedObject filename_array;
int cur_idx;
BuildDirEntListState(ScriptInterface* scriptInterface)
: pScriptInterface(scriptInterface),
filename_array(scriptInterface->GetJSRuntime()),
cur_idx(0)
{
ScriptInterface::Request rq(pScriptInterface);
filename_array = JS_NewArrayObject(rq.cx, JS::HandleValueArray::empty());
}
};
// called for each matching directory entry; add its full pathname to array.
static Status BuildDirEntListCB(const VfsPath& pathname, const CFileInfo& UNUSED(fileINfo), uintptr_t cbData)
{
BuildDirEntListState* s = (BuildDirEntListState*)cbData;
ScriptInterface::Request rq(s->pScriptInterface);
JS::RootedObject filenameArrayObj(rq.cx, s->filename_array);
JS::RootedValue val(rq.cx);
ScriptInterface::ToJSVal(rq, &val, CStrW(pathname.string()) );
JS_SetElement(rq.cx, filenameArrayObj, s->cur_idx++, val);
return INFO::OK;
}
// Return an array of pathname strings, one for each matching entry in the
// specified directory.
// filter_string: default "" matches everything; otherwise, see vfs_next_dirent.
// recurse: should subdirectories be included in the search? default false.
JS::Value JSI_VFS::BuildDirEntList(ScriptInterface::CxPrivate* pCxPrivate, const std::vector<CStrW>& validPaths, const std::wstring& path, const std::wstring& filterStr, bool recurse)
{
if (!PathRestrictionMet(pCxPrivate, validPaths, path))
return JS::NullValue();
// convert to const wchar_t*; if there's no filter, pass 0 for speed
// (interpreted as: "accept all files without comparing").
const wchar_t* filter = 0;
if (!filterStr.empty())
filter = filterStr.c_str();
int flags = recurse ? vfs::DIR_RECURSIVE : 0;
// build array in the callback function
BuildDirEntListState state(pCxPrivate->pScriptInterface);
vfs::ForEachFile(g_VFS, path, BuildDirEntListCB, (uintptr_t)&state, filter, flags);
return JS::ObjectValue(*state.filename_array);
}
// Return true iff the file exits
bool JSI_VFS::FileExists(ScriptInterface::CxPrivate* pCxPrivate, const std::vector<CStrW>& validPaths, const CStrW& filename)
{
return PathRestrictionMet(pCxPrivate, validPaths, filename) && g_VFS->GetFileInfo(filename, 0) == INFO::OK;
}
// Return time [seconds since 1970] of the last modification to the specified file.
double JSI_VFS::GetFileMTime(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& filename)
{
CFileInfo fileInfo;
Status err = g_VFS->GetFileInfo(filename, &fileInfo);
JS_CHECK_FILE_ERR(err);
return (double)fileInfo.MTime();
}
// Return current size of file.
unsigned int JSI_VFS::GetFileSize(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& filename)
{
CFileInfo fileInfo;
Status err = g_VFS->GetFileInfo(filename, &fileInfo);
JS_CHECK_FILE_ERR(err);
return (unsigned int)fileInfo.Size();
}
// Return file contents in a string. Assume file is UTF-8 encoded text.
JS::Value JSI_VFS::ReadFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filename)
{
CVFSFile file;
if (file.Load(g_VFS, filename) != PSRETURN_OK)
return JS::NullValue();
CStr contents = file.DecodeUTF8(); // assume it's UTF-8
// Fix CRLF line endings. (This function will only ever be used on text files.)
contents.Replace("\r\n", "\n");
// Decode as UTF-8
ScriptInterface::Request rq(pCxPrivate);
JS::RootedValue ret(rq.cx);
ScriptInterface::ToJSVal(rq, &ret, contents.FromUTF8());
return ret;
}
// Return file contents as an array of lines. Assume file is UTF-8 encoded text.
JS::Value JSI_VFS::ReadFileLines(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filename)
{
CVFSFile file;
if (file.Load(g_VFS, filename) != PSRETURN_OK)
return JS::NullValue();
CStr contents = file.DecodeUTF8(); // assume it's UTF-8
// Fix CRLF line endings. (This function will only ever be used on text files.)
contents.Replace("\r\n", "\n");
// split into array of strings (one per line)
std::stringstream ss(contents);
const ScriptInterface& scriptInterface = *pCxPrivate->pScriptInterface;
ScriptInterface::Request rq(scriptInterface);
JS::RootedValue line_array(rq.cx);
ScriptInterface::CreateArray(rq, &line_array);
std::string line;
int cur_line = 0;
while (std::getline(ss, line))
{
// Decode each line as UTF-8
JS::RootedValue val(rq.cx);
ScriptInterface::ToJSVal(rq, &val, CStr(line).FromUTF8());
scriptInterface.SetPropertyInt(line_array, cur_line++, val);
}
return line_array;
}
JS::Value JSI_VFS::ReadJSONFile(ScriptInterface::CxPrivate* pCxPrivate, const std::vector<CStrW>& validPaths, const CStrW& filePath)
{
if (!PathRestrictionMet(pCxPrivate, validPaths, filePath))
return JS::NullValue();
const ScriptInterface& scriptInterface = *pCxPrivate->pScriptInterface;
ScriptInterface::Request rq(scriptInterface);
JS::RootedValue out(rq.cx);
scriptInterface.ReadJSONFile(filePath, &out);
return out;
}
void JSI_VFS::WriteJSONFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath, JS::HandleValue val1)
{
const ScriptInterface& scriptInterface = *pCxPrivate->pScriptInterface;
ScriptInterface::Request rq(scriptInterface);
// TODO: This is a workaround because we need to pass a MutableHandle to StringifyJSON.
JS::RootedValue val(rq.cx, val1);
std::string str(scriptInterface.StringifyJSON(&val, false));
VfsPath path(filePath);
WriteBuffer buf;
buf.Append(str.c_str(), str.length());
g_VFS->CreateFile(path, buf.Data(), buf.Size());
}
bool JSI_VFS::PathRestrictionMet(ScriptInterface::CxPrivate* pCxPrivate, const std::vector<CStrW>& validPaths, const CStrW& filePath)
{
for (const CStrW& validPath : validPaths)
if (filePath.find(validPath) == 0)
return true;
CStrW allowedPaths;
for (std::size_t i = 0; i < validPaths.size(); ++i)
{
if (i != 0)
allowedPaths += L", ";
allowedPaths += L"\"" + validPaths[i] + L"\"";
}
ScriptInterface::Request rq(pCxPrivate);
JS_ReportError(rq.cx, "This part of the engine may only read from %s!", utf8_from_wstring(allowedPaths).c_str());
return false;
}
#define VFS_ScriptFunctions(context)\
JS::Value Script_ReadJSONFile_##context(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath)\
{\
return JSI_VFS::ReadJSONFile(pCxPrivate, PathRestriction_##context, filePath);\
}\
JS::Value Script_ListDirectoryFiles_##context(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& path, const std::wstring& filterStr, bool recurse)\
{\
return JSI_VFS::BuildDirEntList(pCxPrivate, PathRestriction_##context, path, filterStr, recurse);\
}\
bool Script_FileExists_##context(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath)\
{\
return JSI_VFS::FileExists(pCxPrivate, PathRestriction_##context, filePath);\
}\
VFS_ScriptFunctions(GUI);
VFS_ScriptFunctions(Simulation);
VFS_ScriptFunctions(Maps);
#undef VFS_ScriptFunctions
void JSI_VFS::RegisterScriptFunctions_GUI(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction<JS::Value, std::wstring, std::wstring, bool, &Script_ListDirectoryFiles_GUI>("ListDirectoryFiles");
scriptInterface.RegisterFunction<bool, std::wstring, Script_FileExists_GUI>("FileExists");
scriptInterface.RegisterFunction<double, std::wstring, &JSI_VFS::GetFileMTime>("GetFileMTime");
scriptInterface.RegisterFunction<unsigned int, std::wstring, &JSI_VFS::GetFileSize>("GetFileSize");
scriptInterface.RegisterFunction<JS::Value, std::wstring, &JSI_VFS::ReadFile>("ReadFile");
scriptInterface.RegisterFunction<JS::Value, std::wstring, &JSI_VFS::ReadFileLines>("ReadFileLines");
scriptInterface.RegisterFunction<JS::Value, std::wstring, &Script_ReadJSONFile_GUI>("ReadJSONFile");
scriptInterface.RegisterFunction<void, std::wstring, JS::HandleValue, &WriteJSONFile>("WriteJSONFile");
}
void JSI_VFS::RegisterScriptFunctions_Simulation(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction<JS::Value, std::wstring, std::wstring, bool, &Script_ListDirectoryFiles_Simulation>("ListDirectoryFiles");
scriptInterface.RegisterFunction<bool, std::wstring, Script_FileExists_Simulation>("FileExists");
scriptInterface.RegisterFunction<JS::Value, std::wstring, &Script_ReadJSONFile_Simulation>("ReadJSONFile");
}
void JSI_VFS::RegisterScriptFunctions_Maps(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction<JS::Value, std::wstring, std::wstring, bool, &Script_ListDirectoryFiles_Maps>("ListDirectoryFiles");
scriptInterface.RegisterFunction<bool, std::wstring, Script_FileExists_Maps>("FileExists");
scriptInterface.RegisterFunction<JS::Value, std::wstring, &Script_ReadJSONFile_Maps>("ReadJSONFile");
}