LCOV - code coverage report
Current view: top level - source/ps/scripting - JSInterface_VFS.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 56 130 43.1 %
Date: 2023-01-19 00:18:29 Functions: 13 36 36.1 %

          Line data    Source code
       1             : /* Copyright (C) 2022 Wildfire Games.
       2             :  * This file is part of 0 A.D.
       3             :  *
       4             :  * 0 A.D. is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 2 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * 0 A.D. is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #include "precompiled.h"
      19             : 
      20             : #include "JSInterface_VFS.h"
      21             : 
      22             : #include "lib/file/vfs/vfs_util.h"
      23             : #include "ps/CLogger.h"
      24             : #include "ps/CStr.h"
      25             : #include "ps/Filesystem.h"
      26             : #include "scriptinterface/FunctionWrapper.h"
      27             : #include "scriptinterface/JSON.h"
      28             : 
      29             : #include <sstream>
      30             : 
      31             : namespace JSI_VFS
      32             : {
      33             : // Only allow engine compartments to read files they may be concerned about.
      34             : #define PathRestriction_GUI {L"gui/", L"simulation/", L"maps/", L"campaigns/", L"saves/campaigns/", L"config/matchsettings.json", L"config/matchsettings.mp.json", L"moddata"}
      35             : #define PathRestriction_Simulation {L"simulation/"}
      36             : #define PathRestriction_Maps {L"simulation/", L"maps/"}
      37             : 
      38             : // shared error handling code
      39             : #define JS_CHECK_FILE_ERR(err)\
      40             :     /* this is liable to happen often, so don't complain */\
      41             :     if (err == ERR::VFS_FILE_NOT_FOUND)\
      42             :     {\
      43             :         return 0; \
      44             :     }\
      45             :     /* unknown failure. We output an error message. */\
      46             :     else if (err < 0)\
      47             :         LOGERROR("Unknown failure in VFS %i", err );
      48             :     /* else: success */
      49             : 
      50             : 
      51             : // Tests whether the current script context is allowed to read from the given directory
      52          90 : bool PathRestrictionMet(const ScriptRequest& rq, const std::vector<CStrW>& validPaths, const CStrW& filePath)
      53             : {
      54          90 :     for (const CStrW& validPath : validPaths)
      55          90 :         if (filePath.find(validPath) == 0)
      56          90 :             return true;
      57             : 
      58           0 :     CStrW allowedPaths;
      59           0 :     for (std::size_t i = 0; i < validPaths.size(); ++i)
      60             :     {
      61           0 :         if (i != 0)
      62           0 :             allowedPaths += L", ";
      63             : 
      64           0 :         allowedPaths += L"\"" + validPaths[i] + L"\"";
      65             :     }
      66             : 
      67           0 :     ScriptException::Raise(rq, "Restricted access to %s. This part of the engine may only read from %s!", utf8_from_wstring(filePath).c_str(), utf8_from_wstring(allowedPaths).c_str());
      68             : 
      69           0 :     return false;
      70             : }
      71             : 
      72             : 
      73             : // state held across multiple BuildDirEntListCB calls; init by BuildDirEntList.
      74           6 : struct BuildDirEntListState
      75             : {
      76             :     const ScriptRequest& rq;
      77             :     JS::PersistentRootedObject filename_array;
      78             :     int cur_idx;
      79             : 
      80           6 :     BuildDirEntListState(const ScriptRequest& rq)
      81           6 :         : rq(rq),
      82           6 :         filename_array(rq.cx),
      83          12 :         cur_idx(0)
      84             :     {
      85           6 :         filename_array = JS::NewArrayObject(rq.cx, JS::HandleValueArray::empty());
      86           6 :     }
      87             : };
      88             : 
      89             : // called for each matching directory entry; add its full pathname to array.
      90          84 : static Status BuildDirEntListCB(const VfsPath& pathname, const CFileInfo& UNUSED(fileINfo), uintptr_t cbData)
      91             : {
      92          84 :     BuildDirEntListState* s = (BuildDirEntListState*)cbData;
      93             : 
      94         168 :     JS::RootedObject filenameArrayObj(s->rq.cx, s->filename_array);
      95         168 :     JS::RootedValue val(s->rq.cx);
      96          84 :     Script::ToJSVal(s->rq, &val, CStrW(pathname.string()) );
      97          84 :     JS_SetElement(s->rq.cx, filenameArrayObj, s->cur_idx++, val);
      98         168 :     return INFO::OK;
      99             : }
     100             : 
     101             : 
     102             : // Return an array of pathname strings, one for each matching entry in the
     103             : // specified directory.
     104             : //   filter_string: default "" matches everything; otherwise, see vfs_next_dirent.
     105             : //   recurse: should subdirectories be included in the search? default false.
     106           6 : JS::Value BuildDirEntList(const ScriptRequest& rq, const std::vector<CStrW>& validPaths, const std::wstring& path, const std::wstring& filterStr, bool recurse)
     107             : {
     108           6 :     if (!PathRestrictionMet(rq, validPaths, path))
     109           0 :         return JS::NullValue();
     110             : 
     111             :     // convert to const wchar_t*; if there's no filter, pass 0 for speed
     112             :     // (interpreted as: "accept all files without comparing").
     113           6 :     const wchar_t* filter = 0;
     114           6 :     if (!filterStr.empty())
     115           6 :         filter = filterStr.c_str();
     116             : 
     117           6 :     int flags = recurse ? vfs::DIR_RECURSIVE : 0;
     118             : 
     119             :     // build array in the callback function
     120          12 :     BuildDirEntListState state(rq);
     121           6 :     vfs::ForEachFile(g_VFS, path, BuildDirEntListCB, (uintptr_t)&state, filter, flags);
     122             : 
     123           6 :     return JS::ObjectValue(*state.filename_array);
     124             : }
     125             : 
     126             : // Return true iff the file exits
     127           0 : bool FileExists(const ScriptRequest& rq, const std::vector<CStrW>& validPaths, const CStrW& filename)
     128             : {
     129           0 :     return PathRestrictionMet(rq, validPaths, filename) && g_VFS->GetFileInfo(filename, 0) == INFO::OK;
     130             : }
     131             : 
     132             : // Return time [seconds since 1970] of the last modification to the specified file.
     133           0 : double GetFileMTime(const std::wstring& filename)
     134             : {
     135           0 :     CFileInfo fileInfo;
     136           0 :     Status err = g_VFS->GetFileInfo(filename, &fileInfo);
     137           0 :     JS_CHECK_FILE_ERR(err);
     138             : 
     139           0 :     return (double)fileInfo.MTime();
     140             : }
     141             : 
     142             : // Return current size of file.
     143           0 : unsigned int GetFileSize(const std::wstring& filename)
     144             : {
     145           0 :     CFileInfo fileInfo;
     146           0 :     Status err = g_VFS->GetFileInfo(filename, &fileInfo);
     147           0 :     JS_CHECK_FILE_ERR(err);
     148             : 
     149           0 :     return (unsigned int)fileInfo.Size();
     150             : }
     151             : 
     152             : // Return file contents in a string. Assume file is UTF-8 encoded text.
     153           0 : JS::Value ReadFile(const ScriptRequest& rq, const std::vector<CStrW>& validPaths, const CStrW& filename)
     154             : {
     155           0 :     if (!PathRestrictionMet(rq, validPaths, filename))
     156           0 :         return JS::NullValue();
     157             : 
     158           0 :     CVFSFile file;
     159           0 :     if (file.Load(g_VFS, filename) != PSRETURN_OK)
     160           0 :         return JS::NullValue();
     161             : 
     162           0 :     CStr contents = file.DecodeUTF8(); // assume it's UTF-8
     163             : 
     164             :     // Fix CRLF line endings. (This function will only ever be used on text files.)
     165           0 :     contents.Replace("\r\n", "\n");
     166             : 
     167             :     // Decode as UTF-8
     168           0 :     JS::RootedValue ret(rq.cx);
     169           0 :     Script::ToJSVal(rq, &ret, contents.FromUTF8());
     170           0 :     return ret;
     171             : }
     172             : 
     173             : // Return file contents as an array of lines. Assume file is UTF-8 encoded text.
     174           0 : JS::Value ReadFileLines(const ScriptRequest& rq, const std::vector<CStrW>& validPaths, const CStrW& filename)
     175             : {
     176           0 :     if (!PathRestrictionMet(rq, validPaths, filename))
     177           0 :         return JS::NullValue();
     178             : 
     179           0 :     CVFSFile file;
     180           0 :     if (file.Load(g_VFS, filename) != PSRETURN_OK)
     181           0 :         return JS::NullValue();
     182             : 
     183           0 :     CStr contents = file.DecodeUTF8(); // assume it's UTF-8
     184             : 
     185             :     // Fix CRLF line endings. (This function will only ever be used on text files.)
     186           0 :     contents.Replace("\r\n", "\n");
     187             : 
     188             :     // split into array of strings (one per line)
     189           0 :     std::stringstream ss(contents);
     190             : 
     191           0 :     JS::RootedValue line_array(rq.cx);
     192           0 :     Script::CreateArray(rq, &line_array);
     193             : 
     194           0 :     std::string line;
     195           0 :     int cur_line = 0;
     196             : 
     197           0 :     while (std::getline(ss, line))
     198             :     {
     199             :         // Decode each line as UTF-8
     200           0 :         JS::RootedValue val(rq.cx);
     201           0 :         Script::ToJSVal(rq, &val, CStr(line).FromUTF8());
     202           0 :         Script::SetPropertyInt(rq, line_array, cur_line++, val);
     203             :     }
     204             : 
     205           0 :     return line_array;
     206             : }
     207             : 
     208             : // Return file contents parsed as a JS Object
     209          84 : JS::Value ReadJSONFile(const ScriptInterface& scriptInterface, const std::vector<CStrW>& validPaths, const CStrW& filePath)
     210             : {
     211         168 :     ScriptRequest rq(scriptInterface);
     212          84 :     if (!PathRestrictionMet(rq, validPaths, filePath))
     213           0 :         return JS::NullValue();
     214             : 
     215         168 :     JS::RootedValue out(rq.cx);
     216          84 :     Script::ReadJSONFile(rq, filePath, &out);
     217          84 :     return out;
     218             : }
     219             : 
     220             : // Save given JS Object to a JSON file
     221           0 : void WriteJSONFile(const ScriptInterface& scriptInterface, const std::vector<CStrW>& validPaths, const CStrW& filePath, JS::HandleValue val1)
     222             : {
     223           0 :     ScriptRequest rq(scriptInterface);
     224           0 :     if (!PathRestrictionMet(rq, validPaths, filePath))
     225           0 :         return;
     226             : 
     227             :     // TODO: This is a workaround because we need to pass a MutableHandle to StringifyJSON.
     228           0 :     JS::RootedValue val(rq.cx, val1);
     229             : 
     230           0 :     std::string str(Script::StringifyJSON(rq, &val, false));
     231             : 
     232           0 :     VfsPath path(filePath);
     233           0 :     WriteBuffer buf;
     234           0 :     buf.Append(str.c_str(), str.length());
     235           0 :     if (g_VFS->CreateFile(path, buf.Data(), buf.Size()) == INFO::OK)
     236             :     {
     237           0 :         OsPath realPath;
     238           0 :         g_VFS->GetRealPath(path, realPath, false);
     239           0 :         debug_printf("FILES| JSON data written to '%s'\n", realPath.string8().c_str());
     240             :     }
     241             :     else
     242           0 :         debug_printf("FILES| Failed to write JSON data to '%s'\n", path.string8().c_str());
     243             : }
     244             : 
     245           0 : bool DeleteCampaignSave(const CStrW& filePath)
     246             : {
     247           0 :     OsPath realPath;
     248           0 :     if (filePath.Left(16) != L"saves/campaigns/" || filePath.Right(12) != L".0adcampaign")
     249           0 :         return false;
     250             : 
     251           0 :     return VfsFileExists(filePath) &&
     252           0 :         g_VFS->GetRealPath(filePath, realPath) == INFO::OK &&
     253           0 :         g_VFS->RemoveFile(filePath) == INFO::OK &&
     254           0 :         wunlink(realPath) == 0;
     255             : }
     256             : 
     257             : #define VFS_ScriptFunctions(context)\
     258             : JS::Value Script_ReadJSONFile_##context(const ScriptInterface& scriptInterface, const std::wstring& filePath)\
     259             : {\
     260             :     return ReadJSONFile(scriptInterface, PathRestriction_##context, filePath);\
     261             : }\
     262             : void Script_WriteJSONFile_##context(const ScriptInterface& scriptInterface, const std::wstring& filePath, JS::HandleValue val1)\
     263             : {\
     264             :     return WriteJSONFile(scriptInterface, PathRestriction_##context, filePath, val1);\
     265             : }\
     266             : JS::Value Script_ReadFile_##context(const ScriptInterface& scriptInterface, const std::wstring& filePath)\
     267             : {\
     268             :     return ReadFile(scriptInterface, PathRestriction_##context, filePath);\
     269             : }\
     270             : JS::Value Script_ReadFileLines_##context(const ScriptInterface& scriptInterface, const std::wstring& filePath)\
     271             : {\
     272             :     return ReadFileLines(scriptInterface, PathRestriction_##context, filePath);\
     273             : }\
     274             : JS::Value Script_ListDirectoryFiles_##context(const ScriptInterface& scriptInterface, const std::wstring& path, const std::wstring& filterStr, bool recurse)\
     275             : {\
     276             :     return BuildDirEntList(scriptInterface, PathRestriction_##context, path, filterStr, recurse);\
     277             : }\
     278             : bool Script_FileExists_##context(const ScriptInterface& scriptInterface, const std::wstring& filePath)\
     279             : {\
     280             :     return FileExists(scriptInterface, PathRestriction_##context, filePath);\
     281             : }\
     282             : 
     283           0 : VFS_ScriptFunctions(GUI);
     284           0 : VFS_ScriptFunctions(Simulation);
     285          90 : VFS_ScriptFunctions(Maps);
     286             : #undef VFS_ScriptFunctions
     287             : 
     288          12 : void RegisterScriptFunctions_ReadWriteAnywhere(const ScriptRequest& rq)
     289             : {
     290          12 :     ScriptFunction::Register<&Script_ListDirectoryFiles_GUI>(rq, "ListDirectoryFiles");
     291          12 :     ScriptFunction::Register<&Script_FileExists_GUI>(rq, "FileExists");
     292          12 :     ScriptFunction::Register<&GetFileMTime>(rq, "GetFileMTime");
     293          12 :     ScriptFunction::Register<&GetFileSize>(rq, "GetFileSize");
     294          12 :     ScriptFunction::Register<&Script_ReadFile_GUI>(rq, "ReadFile");
     295          12 :     ScriptFunction::Register<&Script_ReadFileLines_GUI>(rq, "ReadFileLines");
     296          12 :     ScriptFunction::Register<&Script_ReadJSONFile_GUI>(rq, "ReadJSONFile");
     297          12 :     ScriptFunction::Register<&Script_WriteJSONFile_GUI>(rq, "WriteJSONFile");
     298          12 :     ScriptFunction::Register<&DeleteCampaignSave>(rq, "DeleteCampaignSave");
     299          12 : }
     300             : 
     301          62 : void RegisterScriptFunctions_ReadOnlySimulation(const ScriptRequest& rq)
     302             : {
     303          62 :     ScriptFunction::Register<&Script_ListDirectoryFiles_Simulation>(rq, "ListDirectoryFiles");
     304          62 :     ScriptFunction::Register<&Script_FileExists_Simulation>(rq, "FileExists");
     305          62 :     ScriptFunction::Register<&Script_ReadJSONFile_Simulation>(rq, "ReadJSONFile");
     306          62 : }
     307             : 
     308           6 : void RegisterScriptFunctions_ReadOnlySimulationMaps(const ScriptRequest& rq)
     309             : {
     310          18 :     ScriptFunction::Register<&Script_ListDirectoryFiles_Maps>(rq, "ListDirectoryFiles");
     311           6 :     ScriptFunction::Register<&Script_FileExists_Maps>(rq, "FileExists");
     312         174 :     ScriptFunction::Register<&Script_ReadJSONFile_Maps>(rq, "ReadJSONFile");
     313           6 : }
     314           3 : }

Generated by: LCOV version 1.13