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 : }
|