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"&lt;"; break;
298 		case '>': ret += L"&gt;"; break;
299 		case '&': ret += L"&amp;"; break;
300 		case '"': ret += L"&quot;"; break;
301 		case '\t': ret += L"&#9;"; break;
302 		case '\n': ret += L"&#10;"; break;
303 		case '\r': ret += L"&#13;"; 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