Line data Source code
1 : /* Copyright (C) 2023 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 "ParamNode.h"
21 :
22 : #include "lib/utf8.h"
23 : #include "ps/CLogger.h"
24 : #include "ps/CStr.h"
25 : #include "ps/CStrIntern.h"
26 : #include "ps/Filesystem.h"
27 : #include "ps/XML/Xeromyces.h"
28 : #include "scriptinterface/ScriptRequest.h"
29 :
30 : #include <sstream>
31 : #include <string_view>
32 :
33 : #include <boost/algorithm/string.hpp>
34 :
35 1 : static CParamNode g_NullNode(false);
36 :
37 624 : CParamNode::CParamNode(bool isOk) :
38 624 : m_IsOk(isOk)
39 : {
40 624 : }
41 :
42 40 : void CParamNode::LoadXML(CParamNode& ret, const XMBData& xmb, const wchar_t* sourceIdentifier /*= NULL*/)
43 : {
44 40 : ret.ApplyLayer(xmb, xmb.GetRoot(), sourceIdentifier);
45 40 : }
46 :
47 9 : void CParamNode::LoadXML(CParamNode& ret, const VfsPath& path, const std::string& validatorName)
48 : {
49 18 : CXeromyces xero;
50 9 : PSRETURN ok = xero.Load(g_VFS, path, validatorName);
51 9 : if (ok != PSRETURN_OK)
52 0 : return; // (Xeromyces already logged an error)
53 :
54 9 : LoadXML(ret, xero, path.string().c_str());
55 : }
56 :
57 54 : PSRETURN CParamNode::LoadXMLString(CParamNode& ret, const char* xml, const wchar_t* sourceIdentifier /*=NULL*/)
58 : {
59 108 : CXeromyces xero;
60 54 : PSRETURN ok = xero.LoadString(xml);
61 54 : if (ok != PSRETURN_OK)
62 0 : return ok;
63 :
64 54 : ret.ApplyLayer(xero, xero.GetRoot(), sourceIdentifier);
65 :
66 54 : return PSRETURN_OK;
67 : }
68 :
69 476 : void CParamNode::ApplyLayer(const XMBData& xmb, const XMBElement& element, const wchar_t* sourceIdentifier /*= NULL*/)
70 : {
71 476 : ResetScriptVal();
72 :
73 930 : std::string name = xmb.GetElementString(element.GetNodeName());
74 930 : CStr value = element.GetText();
75 :
76 476 : bool hasSetValue = false;
77 :
78 : // Look for special attributes
79 476 : int at_disable = xmb.GetAttributeID("disable");
80 476 : int at_replace = xmb.GetAttributeID("replace");
81 476 : int at_filtered = xmb.GetAttributeID("filtered");
82 476 : int at_merge = xmb.GetAttributeID("merge");
83 476 : int at_op = xmb.GetAttributeID("op");
84 476 : int at_datatype = xmb.GetAttributeID("datatype");
85 : enum op {
86 : INVALID,
87 : ADD,
88 : MUL,
89 : MUL_ROUND
90 476 : } op = INVALID;
91 476 : bool replacing = false;
92 476 : bool filtering = false;
93 476 : bool merging = false;
94 : {
95 589 : XERO_ITER_ATTR(element, attr)
96 : {
97 135 : if (attr.Name == at_disable)
98 : {
99 2 : m_Childs.erase(name);
100 2 : return;
101 : }
102 133 : else if (attr.Name == at_replace)
103 : {
104 4 : m_Childs.erase(name);
105 4 : replacing = true;
106 : }
107 129 : else if (attr.Name == at_filtered)
108 : {
109 5 : filtering = true;
110 : }
111 124 : else if (attr.Name == at_merge)
112 : {
113 30 : if (m_Childs.find(name) == m_Childs.end())
114 20 : return;
115 10 : merging = true;
116 : }
117 94 : else if (attr.Name == at_op)
118 : {
119 3 : if (attr.Value == "add")
120 1 : op = ADD;
121 2 : else if (attr.Value == "mul")
122 1 : op = MUL;
123 1 : else if (attr.Value == "mul_round")
124 1 : op = MUL_ROUND;
125 : else
126 0 : LOGWARNING("Invalid op '%ls'", attr.Value);
127 : }
128 : }
129 : }
130 : {
131 556 : XERO_ITER_ATTR(element, attr)
132 : {
133 112 : if (attr.Name == at_datatype && attr.Value == "tokens")
134 : {
135 10 : CParamNode& node = m_Childs[name];
136 :
137 : // Split into tokens
138 20 : std::vector<std::string> oldTokens;
139 20 : std::vector<std::string> newTokens;
140 10 : if (!replacing && !node.m_Value.empty()) // ignore the old tokens if replace="" was given
141 2 : boost::algorithm::split(oldTokens, node.m_Value, boost::algorithm::is_space(), boost::algorithm::token_compress_on);
142 10 : if (!value.empty())
143 10 : boost::algorithm::split(newTokens, value, boost::algorithm::is_space(), boost::algorithm::token_compress_on);
144 :
145 : // Merge the two lists
146 20 : std::vector<std::string> tokens = oldTokens;
147 35 : for (const std::string& newToken : newTokens)
148 : {
149 25 : if (newToken[0] == '-')
150 : {
151 : std::vector<std::string>::iterator tokenIt =
152 : std::find(tokens.begin(), tokens.end(),
153 3 : std::string_view{newToken}.substr(1));
154 3 : if (tokenIt != tokens.end())
155 1 : tokens.erase(tokenIt);
156 : else
157 : {
158 : const std::string identifier{
159 2 : sourceIdentifier ? (" in '" +
160 6 : utf8_from_wstring(sourceIdentifier) + "'") : ""};
161 2 : LOGWARNING("[ParamNode] Could not remove token "
162 : "'%s' from node '%s'%s; not present in "
163 : "list nor inherited (possible typo?)",
164 : std::string_view{newToken}.substr(1), name,
165 : identifier);
166 : }
167 : }
168 : else
169 : {
170 22 : if (std::find(oldTokens.begin(), oldTokens.end(), newToken) == oldTokens.end())
171 22 : tokens.push_back(newToken);
172 : }
173 : }
174 :
175 10 : node.m_Value = boost::algorithm::join(tokens, " ");
176 10 : hasSetValue = true;
177 10 : break;
178 : }
179 : }
180 : }
181 :
182 : // Add this element as a child node
183 454 : CParamNode& node = m_Childs[name];
184 454 : if (op != INVALID)
185 : {
186 : // TODO: Support parsing of data types other than fixed; log warnings in other cases
187 3 : fixed oldval = node.ToFixed();
188 3 : fixed mod = fixed::FromString(value);
189 :
190 3 : switch (op)
191 : {
192 1 : case ADD:
193 1 : node.m_Value = (oldval + mod).ToString();
194 1 : break;
195 1 : case MUL:
196 1 : node.m_Value = oldval.Multiply(mod).ToString();
197 1 : break;
198 1 : case MUL_ROUND:
199 1 : node.m_Value = fixed::FromInt(oldval.Multiply(mod).ToInt_RoundToNearest()).ToString();
200 1 : break;
201 0 : default:
202 0 : break;
203 : }
204 3 : hasSetValue = true;
205 : }
206 :
207 454 : if (!hasSetValue && !merging)
208 431 : node.m_Value = value;
209 :
210 : // We also need to reset node's script val, even if it has no children
211 : // or if the attributes change.
212 454 : node.ResetScriptVal();
213 :
214 : // For the filtered case
215 908 : ChildrenMap childs;
216 :
217 : // Recurse through the element's children
218 836 : XERO_ITER_EL(element, child)
219 : {
220 382 : node.ApplyLayer(xmb, child, sourceIdentifier);
221 382 : if (filtering)
222 : {
223 70 : std::string childname = xmb.GetElementString(child.GetNodeName());
224 35 : if (node.m_Childs.find(childname) != node.m_Childs.end())
225 17 : childs[childname] = std::move(node.m_Childs[childname]);
226 : }
227 : }
228 :
229 454 : if (filtering)
230 5 : node.m_Childs.swap(childs);
231 :
232 : // Add the element's attributes, prefixing names with "@"
233 545 : XERO_ITER_ATTR(element, attr)
234 : {
235 : // Skip special attributes
236 113 : if (attr.Name == at_replace || attr.Name == at_op || attr.Name == at_merge || attr.Name == at_filtered)
237 22 : continue;
238 : // Add any others
239 91 : const char* attrName(xmb.GetAttributeString(attr.Name));
240 91 : node.m_Childs[CStr("@") + attrName].m_Value = attr.Value;
241 : }
242 :
243 454 : node.m_Name = name;
244 : }
245 :
246 27 : const CParamNode& CParamNode::GetOnlyChild() const
247 : {
248 27 : if (m_Childs.empty())
249 0 : return g_NullNode;
250 :
251 27 : ENSURE(m_Childs.size() == 1);
252 27 : return m_Childs.begin()->second;
253 : }
254 :
255 328 : const CParamNode& CParamNode::GetChild(const char* name) const
256 : {
257 328 : ChildrenMap::const_iterator it = m_Childs.find(name);
258 328 : if (it == m_Childs.end())
259 129 : return g_NullNode;
260 199 : return it->second;
261 : }
262 :
263 226 : bool CParamNode::IsOk() const
264 : {
265 226 : return m_IsOk;
266 : }
267 :
268 0 : const std::wstring CParamNode::ToWString() const
269 : {
270 0 : return wstring_from_utf8(m_Value);
271 : }
272 :
273 11 : const std::string& CParamNode::ToString() const
274 : {
275 11 : return m_Value;
276 : }
277 :
278 0 : const CStrIntern CParamNode::ToUTF8Intern() const
279 : {
280 0 : return CStrIntern(m_Value);
281 : }
282 :
283 24 : int CParamNode::ToInt() const
284 : {
285 24 : return std::strtol(m_Value.c_str(), nullptr, 10);
286 : }
287 :
288 53 : fixed CParamNode::ToFixed() const
289 : {
290 53 : return fixed::FromString(m_Value);
291 : }
292 :
293 0 : float CParamNode::ToFloat() const
294 : {
295 0 : return std::strtof(m_Value.c_str(), nullptr);
296 : }
297 :
298 8 : bool CParamNode::ToBool() const
299 : {
300 8 : if (m_Value == "true")
301 2 : return true;
302 : else
303 6 : return false;
304 : }
305 :
306 21 : const CParamNode::ChildrenMap& CParamNode::GetChildren() const
307 : {
308 21 : return m_Childs;
309 : }
310 :
311 0 : const std::string& CParamNode::GetName() const
312 : {
313 0 : return m_Name;
314 : }
315 :
316 38 : std::string CParamNode::EscapeXMLString(const std::string& str)
317 : {
318 38 : std::string ret;
319 38 : ret.reserve(str.size());
320 : // TODO: would be nice to check actual v1.0 XML codepoints,
321 : // but our UTF8 validation routines are lacking.
322 187 : for (size_t i = 0; i < str.size(); ++i)
323 : {
324 149 : char c = str[i];
325 149 : switch (c)
326 : {
327 4 : case '<': ret += "<"; break;
328 2 : case '>': ret += ">"; break;
329 1 : case '&': ret += "&"; break;
330 1 : case '"': ret += """; break;
331 1 : case '\t': ret += "	"; break;
332 1 : case '\n': ret += " "; break;
333 1 : case '\r': ret += " "; break;
334 138 : default:
335 138 : ret += c;
336 : }
337 : }
338 38 : return ret;
339 : }
340 :
341 35 : std::string CParamNode::ToXMLString() const
342 : {
343 70 : std::stringstream strm;
344 35 : ToXMLString(strm);
345 70 : return strm.str();
346 : }
347 :
348 215 : void CParamNode::ToXMLString(std::ostream& strm) const
349 : {
350 215 : strm << m_Value;
351 :
352 215 : ChildrenMap::const_iterator it = m_Childs.begin();
353 645 : for (; it != m_Childs.end(); ++it)
354 : {
355 : // Skip attributes here (they were handled when the caller output the tag)
356 215 : if (it->first.length() && it->first[0] == '@')
357 35 : continue;
358 :
359 180 : strm << "<" << it->first;
360 :
361 : // Output the child's attributes first
362 180 : ChildrenMap::const_iterator cit = it->second.m_Childs.begin();
363 526 : for (; cit != it->second.m_Childs.end(); ++cit)
364 : {
365 173 : if (cit->first.length() && cit->first[0] == '@')
366 : {
367 68 : std::string attrname (cit->first.begin()+1, cit->first.end());
368 34 : strm << " " << attrname << "=\"" << EscapeXMLString(cit->second.m_Value) << "\"";
369 : }
370 : }
371 :
372 180 : strm << ">";
373 :
374 180 : it->second.ToXMLString(strm);
375 :
376 180 : strm << "</" << it->first << ">";
377 : }
378 215 : }
379 :
380 57 : void CParamNode::ToJSVal(const ScriptRequest& rq, bool cacheValue, JS::MutableHandleValue ret) const
381 : {
382 57 : if (cacheValue && m_ScriptVal != NULL)
383 : {
384 23 : ret.set(*m_ScriptVal);
385 23 : return;
386 : }
387 :
388 34 : ConstructJSVal(rq, ret);
389 :
390 34 : if (cacheValue)
391 34 : m_ScriptVal.reset(new JS::PersistentRootedValue(rq.cx, ret));
392 : }
393 :
394 83 : void CParamNode::ConstructJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret) const
395 : {
396 83 : if (m_Childs.empty())
397 : {
398 : // Empty node - map to undefined
399 58 : if (m_Value.empty())
400 : {
401 16 : ret.setUndefined();
402 16 : return;
403 : }
404 :
405 : // Just a string
406 84 : JS::RootedString str(rq.cx, JS_NewStringCopyUTF8Z(rq.cx, JS::ConstUTF8CharsZ(m_Value.data(), m_Value.size())));
407 42 : str.set(JS_AtomizeAndPinJSString(rq.cx, str));
408 42 : if (str)
409 : {
410 42 : ret.setString(str);
411 42 : return;
412 : }
413 : // TODO: report error
414 0 : ret.setUndefined();
415 0 : return;
416 : }
417 :
418 : // Got child nodes - convert this node into a hash-table-style object:
419 :
420 50 : JS::RootedObject obj(rq.cx, JS_NewPlainObject(rq.cx));
421 25 : if (!obj)
422 : {
423 0 : ret.setUndefined();
424 0 : return; // TODO: report error
425 : }
426 :
427 50 : JS::RootedValue childVal(rq.cx);
428 74 : for (std::map<std::string, CParamNode>::const_iterator it = m_Childs.begin(); it != m_Childs.end(); ++it)
429 : {
430 49 : it->second.ConstructJSVal(rq, &childVal);
431 49 : if (!JS_SetProperty(rq.cx, obj, it->first.c_str(), childVal))
432 : {
433 0 : ret.setUndefined();
434 0 : return; // TODO: report error
435 : }
436 : }
437 :
438 : // If the node has a string too, add that as an extra property
439 25 : if (!m_Value.empty())
440 : {
441 8 : std::u16string text(m_Value.begin(), m_Value.end());
442 8 : JS::RootedString str(rq.cx, JS_AtomizeAndPinUCStringN(rq.cx, text.c_str(), text.length()));
443 4 : if (!str)
444 : {
445 0 : ret.setUndefined();
446 0 : return; // TODO: report error
447 : }
448 :
449 8 : JS::RootedValue subChildVal(rq.cx, JS::StringValue(str));
450 4 : if (!JS_SetProperty(rq.cx, obj, "_string", subChildVal))
451 : {
452 0 : ret.setUndefined();
453 0 : return; // TODO: report error
454 : }
455 : }
456 :
457 25 : ret.setObject(*obj);
458 : }
459 :
460 930 : void CParamNode::ResetScriptVal()
461 : {
462 930 : m_ScriptVal = NULL;
463 933 : }
|