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