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 "ConfigDB.h"
21 :
22 : #include "lib/allocators/shared_ptr.h"
23 : #include "lib/file/vfs/vfs_path.h"
24 : #include "ps/CLogger.h"
25 : #include "ps/CStr.h"
26 : #include "ps/Filesystem.h"
27 :
28 : #include <boost/algorithm/string.hpp>
29 : #include <mutex>
30 : #include <unordered_set>
31 :
32 : namespace
33 : {
34 :
35 16 : void TriggerAllHooks(const std::multimap<CStr, std::function<void()>>& hooks, const CStr& name)
36 : {
37 16 : std::for_each(hooks.lower_bound(name), hooks.upper_bound(name), [](const std::pair<CStr, std::function<void()>>& hook) { hook.second(); });
38 16 : }
39 :
40 : // These entries will not be printed to logfiles, so that logfiles can be shared without leaking personal or sensitive data
41 4 : const std::unordered_set<std::string> g_UnloggedEntries = {
42 : "lobby.password",
43 : "lobby.buddies",
44 : "userreport.id" // authentication token for GDPR personal data requests
45 3 : };
46 :
47 : #define CHECK_NS(rval)\
48 : do {\
49 : if (ns < 0 || ns >= CFG_LAST)\
50 : {\
51 : debug_warn(L"CConfigDB: Invalid ns value");\
52 : return rval;\
53 : }\
54 : } while (false)
55 :
56 1 : template<typename T> void Get(const CStr& value, T& ret)
57 : {
58 2 : std::stringstream ss(value);
59 1 : ss >> ret;
60 1 : }
61 :
62 0 : template<> void Get<>(const CStr& value, bool& ret)
63 : {
64 0 : ret = value == "true";
65 0 : }
66 :
67 7 : template<> void Get<>(const CStr& value, std::string& ret)
68 : {
69 7 : ret = value;
70 7 : }
71 :
72 16 : std::string EscapeString(const CStr& str)
73 : {
74 16 : std::string ret;
75 52 : for (size_t i = 0; i < str.length(); ++i)
76 : {
77 36 : if (str[i] == '\\')
78 0 : ret += "\\\\";
79 36 : else if (str[i] == '"')
80 0 : ret += "\\\"";
81 : else
82 36 : ret += str[i];
83 : }
84 16 : return ret;
85 : }
86 :
87 : } // anonymous namespace
88 :
89 : typedef std::map<CStr, CConfigValueSet> TConfigMap;
90 :
91 : #define GETVAL(type)\
92 : void CConfigDB::GetValue(EConfigNamespace ns, const CStr& name, type& value)\
93 : {\
94 : CHECK_NS(;);\
95 : std::lock_guard<std::recursive_mutex> s(m_Mutex);\
96 : TConfigMap::iterator it = m_Map[CFG_COMMAND].find(name);\
97 : if (it != m_Map[CFG_COMMAND].end())\
98 : {\
99 : if (!it->second.empty())\
100 : Get(it->second[0], value);\
101 : return;\
102 : }\
103 : for (int search_ns = ns; search_ns >= 0; --search_ns)\
104 : {\
105 : it = m_Map[search_ns].find(name);\
106 : if (it != m_Map[search_ns].end())\
107 : {\
108 : if (!it->second.empty())\
109 : Get(it->second[0], value);\
110 : return;\
111 : }\
112 : }\
113 : }
114 30 : GETVAL(bool)
115 38 : GETVAL(int)
116 0 : GETVAL(u32)
117 12 : GETVAL(float)
118 0 : GETVAL(double)
119 8 : GETVAL(std::string)
120 : #undef GETVAL
121 :
122 1 : std::unique_ptr<CConfigDB> g_ConfigDBPtr;
123 :
124 8 : void CConfigDB::Initialise()
125 : {
126 8 : g_ConfigDBPtr = std::make_unique<CConfigDB>();
127 8 : }
128 :
129 8 : void CConfigDB::Shutdown()
130 : {
131 8 : g_ConfigDBPtr.reset();
132 8 : }
133 :
134 5 : bool CConfigDB::IsInitialised()
135 : {
136 5 : return !!g_ConfigDBPtr;
137 : }
138 :
139 90 : CConfigDB* CConfigDB::Instance()
140 : {
141 90 : return g_ConfigDBPtr.get();
142 : }
143 :
144 16 : CConfigDB::CConfigDB()
145 : {
146 16 : m_HasChanges.fill(false);
147 16 : }
148 :
149 16 : CConfigDB::~CConfigDB()
150 : {
151 16 : }
152 :
153 0 : bool CConfigDB::HasChanges(EConfigNamespace ns) const
154 : {
155 0 : CHECK_NS(false);
156 :
157 0 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
158 0 : return m_HasChanges[ns];
159 : }
160 :
161 0 : void CConfigDB::SetChanges(EConfigNamespace ns, bool value)
162 : {
163 0 : CHECK_NS(;);
164 :
165 0 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
166 0 : m_HasChanges[ns] = value;
167 : }
168 :
169 0 : void CConfigDB::GetValues(EConfigNamespace ns, const CStr& name, CConfigValueSet& values) const
170 : {
171 0 : CHECK_NS(;);
172 :
173 0 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
174 0 : TConfigMap::const_iterator it = m_Map[CFG_COMMAND].find(name);
175 0 : if (it != m_Map[CFG_COMMAND].end())
176 : {
177 0 : values = it->second;
178 0 : return;
179 : }
180 :
181 0 : for (int search_ns = ns; search_ns >= 0; --search_ns)
182 : {
183 0 : it = m_Map[search_ns].find(name);
184 0 : if (it != m_Map[search_ns].end())
185 : {
186 0 : values = it->second;
187 0 : return;
188 : }
189 : }
190 : }
191 :
192 0 : EConfigNamespace CConfigDB::GetValueNamespace(EConfigNamespace ns, const CStr& name) const
193 : {
194 0 : CHECK_NS(CFG_LAST);
195 :
196 0 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
197 0 : TConfigMap::const_iterator it = m_Map[CFG_COMMAND].find(name);
198 0 : if (it != m_Map[CFG_COMMAND].end())
199 0 : return CFG_COMMAND;
200 :
201 0 : for (int search_ns = ns; search_ns >= 0; --search_ns)
202 : {
203 0 : it = m_Map[search_ns].find(name);
204 0 : if (it != m_Map[search_ns].end())
205 0 : return (EConfigNamespace)search_ns;
206 : }
207 :
208 0 : return CFG_LAST;
209 : }
210 :
211 4 : std::map<CStr, CConfigValueSet> CConfigDB::GetValuesWithPrefix(EConfigNamespace ns, const CStr& prefix) const
212 : {
213 8 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
214 4 : std::map<CStr, CConfigValueSet> ret;
215 :
216 4 : CHECK_NS(ret);
217 :
218 : // Loop upwards so that values in later namespaces can override
219 : // values in earlier namespaces
220 28 : for (int search_ns = 0; search_ns <= ns; ++search_ns)
221 36 : for (const std::pair<const CStr, CConfigValueSet>& p : m_Map[search_ns])
222 12 : if (boost::algorithm::starts_with(p.first, prefix))
223 12 : ret[p.first] = p.second;
224 :
225 4 : for (const std::pair<const CStr, CConfigValueSet>& p : m_Map[CFG_COMMAND])
226 0 : if (boost::algorithm::starts_with(p.first, prefix))
227 0 : ret[p.first] = p.second;
228 :
229 4 : return ret;
230 : }
231 :
232 16 : void CConfigDB::SetValueString(EConfigNamespace ns, const CStr& name, const CStr& value)
233 : {
234 16 : CHECK_NS(;);
235 :
236 32 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
237 16 : TConfigMap::iterator it = m_Map[ns].find(name);
238 16 : if (it == m_Map[ns].end())
239 16 : it = m_Map[ns].insert(m_Map[ns].begin(), make_pair(name, CConfigValueSet(1)));
240 :
241 16 : if (!it->second.empty())
242 16 : it->second[0] = value;
243 : else
244 0 : it->second.emplace_back(value);
245 :
246 16 : TriggerAllHooks(m_Hooks, name);
247 : }
248 :
249 0 : void CConfigDB::SetValueBool(EConfigNamespace ns, const CStr& name, const bool value)
250 : {
251 0 : CStr valueString = value ? "true" : "false";
252 0 : SetValueString(ns, name, valueString);
253 0 : }
254 :
255 4 : void CConfigDB::SetValueList(EConfigNamespace ns, const CStr& name, std::vector<CStr> values)
256 : {
257 4 : CHECK_NS(;);
258 :
259 8 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
260 4 : TConfigMap::iterator it = m_Map[ns].find(name);
261 4 : if (it == m_Map[ns].end())
262 4 : it = m_Map[ns].insert(m_Map[ns].begin(), make_pair(name, CConfigValueSet(1)));
263 :
264 4 : it->second = values;
265 : }
266 :
267 0 : bool CConfigDB::RemoveValue(EConfigNamespace ns, const CStr& name)
268 : {
269 0 : CHECK_NS(false);
270 :
271 0 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
272 0 : TConfigMap::iterator it = m_Map[ns].find(name);
273 0 : if (it == m_Map[ns].end())
274 0 : return false;
275 0 : m_Map[ns].erase(it);
276 :
277 0 : TriggerAllHooks(m_Hooks, name);
278 0 : return true;
279 : }
280 :
281 2 : void CConfigDB::SetConfigFile(EConfigNamespace ns, const VfsPath& path)
282 : {
283 2 : CHECK_NS(;);
284 :
285 4 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
286 2 : m_ConfigFile[ns] = path;
287 : }
288 :
289 7 : bool CConfigDB::Reload(EConfigNamespace ns)
290 : {
291 7 : CHECK_NS(false);
292 :
293 14 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
294 :
295 14 : std::shared_ptr<u8> buffer;
296 : size_t buflen;
297 : {
298 : // Handle missing files quietly
299 7 : if (g_VFS->GetFileInfo(m_ConfigFile[ns], NULL) < 0)
300 : {
301 3 : LOGMESSAGE("Cannot find config file \"%s\" - ignoring", m_ConfigFile[ns].string8());
302 3 : return false;
303 : }
304 :
305 4 : LOGMESSAGE("Loading config file \"%s\"", m_ConfigFile[ns].string8());
306 4 : Status ret = g_VFS->LoadFile(m_ConfigFile[ns], buffer, buflen);
307 4 : if (ret != INFO::OK)
308 : {
309 0 : LOGERROR("CConfigDB::Reload(): vfs_load for \"%s\" failed: return was %lld", m_ConfigFile[ns].string8(), (long long)ret);
310 0 : return false;
311 : }
312 : }
313 :
314 8 : TConfigMap newMap;
315 4 : char *filebuf = (char*)buffer.get();
316 4 : char *filebufend = filebuf+buflen;
317 :
318 4 : bool quoted = false;
319 8 : CStr header;
320 8 : CStr name;
321 8 : CStr value;
322 4 : int line = 1;
323 8 : std::vector<CStr> values;
324 32 : for (char* pos = filebuf; pos < filebufend; ++pos)
325 : {
326 54 : switch (*pos)
327 : {
328 0 : case '\n':
329 : case ';':
330 0 : break; // We finished parsing this line
331 :
332 2 : case ' ':
333 : case '\r':
334 : case '\t':
335 2 : continue; // ignore
336 :
337 0 : case '[':
338 0 : header.clear();
339 0 : for (++pos; pos < filebufend && *pos != '\n' && *pos != ']'; ++pos)
340 0 : header.push_back(*pos);
341 :
342 0 : if (pos == filebufend || *pos == '\n')
343 : {
344 0 : LOGERROR("Config header with missing close tag encountered on line %d in '%s'", line, m_ConfigFile[ns].string8());
345 0 : header.clear();
346 0 : ++line;
347 0 : continue;
348 : }
349 :
350 0 : LOGMESSAGE("Found config header '%s'", header.c_str());
351 0 : header.push_back('.');
352 0 : while (++pos < filebufend && *pos != '\n' && *pos != ';')
353 0 : if (*pos != ' ' && *pos != '\r')
354 : {
355 0 : LOGERROR("Config settings on the same line as a header on line %d in '%s'", line, m_ConfigFile[ns].string8());
356 0 : break;
357 : }
358 0 : while (pos < filebufend && *pos != '\n')
359 0 : ++pos;
360 0 : ++line;
361 0 : continue;
362 :
363 2 : case '=':
364 : // Parse parameters (comma separated, possibly quoted)
365 6 : for (++pos; pos < filebufend && *pos != '\n' && *pos != ';'; ++pos)
366 : {
367 4 : switch (*pos)
368 : {
369 2 : case '"':
370 2 : quoted = true;
371 : // parse until not quoted anymore
372 3 : for (++pos; pos < filebufend && *pos != '\n' && *pos != '"'; ++pos)
373 : {
374 1 : if (*pos == '\\' && ++pos == filebufend)
375 : {
376 0 : LOGERROR("Escape character at end of input (line %d in '%s')", line, m_ConfigFile[ns].string8());
377 0 : break;
378 : }
379 :
380 1 : value.push_back(*pos);
381 : }
382 2 : if (pos < filebufend && *pos == '"')
383 2 : quoted = false;
384 : else
385 0 : --pos; // We should terminate the outer loop too
386 2 : break;
387 :
388 2 : case ' ':
389 : case '\r':
390 : case '\t':
391 2 : break; // ignore
392 :
393 0 : case ',':
394 0 : if (!value.empty())
395 0 : values.push_back(value);
396 0 : value.clear();
397 0 : break;
398 :
399 0 : default:
400 0 : value.push_back(*pos);
401 0 : break;
402 : }
403 : }
404 2 : if (quoted) // We ignore the invalid parameter
405 0 : LOGERROR("Unmatched quote while parsing config file '%s' on line %d", m_ConfigFile[ns].string8(), line);
406 2 : else if (!value.empty())
407 1 : values.push_back(value);
408 2 : value.clear();
409 2 : quoted = false;
410 2 : break; // We are either at the end of the line, or we still have a comment to parse
411 :
412 24 : default:
413 24 : name.push_back(*pos);
414 24 : continue;
415 : }
416 :
417 : // Consume the rest of the line
418 2 : while (pos < filebufend && *pos != '\n')
419 0 : ++pos;
420 : // Store the setting
421 2 : if (!name.empty())
422 : {
423 4 : CStr key(header + name);
424 2 : newMap[key] = values;
425 2 : if (g_UnloggedEntries.find(key) != g_UnloggedEntries.end())
426 0 : LOGMESSAGE("Loaded config string \"%s\"", key);
427 2 : else if (values.empty())
428 1 : LOGMESSAGE("Loaded config string \"%s\" = (empty)", key);
429 : else
430 : {
431 2 : std::string vals;
432 1 : for (size_t i = 0; i + 1 < newMap[key].size(); ++i)
433 0 : vals += "\"" + EscapeString(newMap[key][i]) + "\", ";
434 1 : vals += "\"" + EscapeString(newMap[key][values.size()-1]) + "\"";
435 1 : LOGMESSAGE("Loaded config string \"%s\" = %s", key, vals);
436 : }
437 : }
438 :
439 2 : name.clear();
440 2 : values.clear();
441 2 : ++line;
442 : }
443 :
444 4 : if (!name.empty())
445 0 : LOGERROR("Config file does not have a new line after the last config setting '%s'", name);
446 :
447 4 : m_Map[ns].swap(newMap);
448 4 : m_HasChanges[ns] = false;
449 :
450 4 : return true;
451 : }
452 :
453 4 : bool CConfigDB::WriteFile(EConfigNamespace ns) const
454 : {
455 4 : CHECK_NS(false);
456 :
457 8 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
458 4 : return WriteFile(ns, m_ConfigFile[ns]);
459 : }
460 :
461 7 : bool CConfigDB::WriteFile(EConfigNamespace ns, const VfsPath& path) const
462 : {
463 7 : CHECK_NS(false);
464 :
465 14 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
466 14 : std::shared_ptr<u8> buf;
467 7 : AllocateAligned(buf, 1*MiB, maxSectorSize);
468 7 : char* pos = (char*)buf.get();
469 :
470 20 : for (const std::pair<const CStr, CConfigValueSet>& p : m_Map[ns])
471 : {
472 : size_t i;
473 13 : pos += sprintf(pos, "%s = ", p.first.c_str());
474 16 : for (i = 0; i + 1 < p.second.size(); ++i)
475 3 : pos += sprintf(pos, "\"%s\", ", EscapeString(p.second[i]).c_str());
476 13 : if (!p.second.empty())
477 12 : pos += sprintf(pos, "\"%s\"\n", EscapeString(p.second[i]).c_str());
478 : else
479 1 : pos += sprintf(pos, "\"\"\n");
480 : }
481 7 : const size_t len = pos - (char*)buf.get();
482 :
483 7 : Status ret = g_VFS->CreateFile(path, buf, len);
484 7 : if (ret < 0)
485 : {
486 0 : LOGERROR("CConfigDB::WriteFile(): CreateFile \"%s\" failed (error: %d)", path.string8(), (int)ret);
487 0 : return false;
488 : }
489 :
490 7 : return true;
491 : }
492 :
493 0 : bool CConfigDB::WriteValueToFile(EConfigNamespace ns, const CStr& name, const CStr& value)
494 : {
495 0 : CHECK_NS(false);
496 :
497 0 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
498 0 : return WriteValueToFile(ns, name, value, m_ConfigFile[ns]);
499 : }
500 :
501 0 : bool CConfigDB::WriteValueToFile(EConfigNamespace ns, const CStr& name, const CStr& value, const VfsPath& path)
502 : {
503 0 : CHECK_NS(false);
504 :
505 0 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
506 :
507 0 : TConfigMap newMap;
508 0 : m_Map[ns].swap(newMap);
509 0 : Reload(ns);
510 :
511 0 : SetValueString(ns, name, value);
512 0 : bool ret = WriteFile(ns, path);
513 0 : m_Map[ns].swap(newMap);
514 0 : return ret;
515 : }
516 :
517 0 : CConfigDBHook CConfigDB::RegisterHookAndCall(const CStr& name, std::function<void()> hook)
518 : {
519 0 : hook();
520 0 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
521 0 : return CConfigDBHook(*this, m_Hooks.emplace(name, hook));
522 : }
523 :
524 0 : void CConfigDB::UnregisterHook(CConfigDBHook&& hook)
525 : {
526 0 : if (hook.m_Ptr != m_Hooks.end())
527 : {
528 0 : std::lock_guard<std::recursive_mutex> s(m_Mutex);
529 0 : m_Hooks.erase(hook.m_Ptr);
530 0 : hook.m_Ptr = m_Hooks.end();
531 : }
532 0 : }
533 :
534 0 : void CConfigDB::UnregisterHook(std::unique_ptr<CConfigDBHook> hook)
535 : {
536 0 : UnregisterHook(std::move(*hook.get()));
537 3 : }
538 :
539 : #undef CHECK_NS
|