/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ParamNode.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/CStrIntern.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" #include "scriptinterface/ScriptRequest.h" #include #include #include #include #include static CParamNode g_NullNode(false); CParamNode::CParamNode(bool isOk) : m_IsOk(isOk) { } void CParamNode::LoadXML(CParamNode& ret, const XMBData& xmb, const wchar_t* sourceIdentifier /*= NULL*/) { ret.ApplyLayer(xmb, xmb.GetRoot(), sourceIdentifier); } void CParamNode::LoadXML(CParamNode& ret, const VfsPath& path, const std::string& validatorName) { CXeromyces xero; PSRETURN ok = xero.Load(g_VFS, path, validatorName); if (ok != PSRETURN_OK) return; // (Xeromyces already logged an error) LoadXML(ret, xero, path.string().c_str()); } PSRETURN CParamNode::LoadXMLString(CParamNode& ret, const char* xml, const wchar_t* sourceIdentifier /*=NULL*/) { CXeromyces xero; PSRETURN ok = xero.LoadString(xml); if (ok != PSRETURN_OK) return ok; ret.ApplyLayer(xero, xero.GetRoot(), sourceIdentifier); return PSRETURN_OK; } void CParamNode::ApplyLayer(const XMBData& xmb, const XMBElement& element, const wchar_t* sourceIdentifier /*= NULL*/) { ResetScriptVal(); std::string name = xmb.GetElementString(element.GetNodeName()); CStr value = element.GetText(); bool hasSetValue = false; // Look for special attributes int at_disable = xmb.GetAttributeID("disable"); int at_replace = xmb.GetAttributeID("replace"); int at_filtered = xmb.GetAttributeID("filtered"); int at_merge = xmb.GetAttributeID("merge"); int at_op = xmb.GetAttributeID("op"); int at_datatype = xmb.GetAttributeID("datatype"); enum op { INVALID, ADD, MUL, MUL_ROUND } op = INVALID; bool replacing = false; bool filtering = false; bool merging = false; { XERO_ITER_ATTR(element, attr) { if (attr.Name == at_disable) { m_Childs.erase(name); return; } else if (attr.Name == at_replace) { m_Childs.erase(name); replacing = true; } else if (attr.Name == at_filtered) { filtering = true; } else if (attr.Name == at_merge) { if (m_Childs.find(name) == m_Childs.end()) return; merging = true; } else if (attr.Name == at_op) { if (attr.Value == "add") op = ADD; else if (attr.Value == "mul") op = MUL; else if (attr.Value == "mul_round") op = MUL_ROUND; else LOGWARNING("Invalid op '%ls'", attr.Value); } } } { XERO_ITER_ATTR(element, attr) { if (attr.Name == at_datatype && attr.Value == "tokens") { CParamNode& node = m_Childs[name]; // Split into tokens std::vector oldTokens; std::vector newTokens; if (!replacing && !node.m_Value.empty()) // ignore the old tokens if replace="" was given boost::algorithm::split(oldTokens, node.m_Value, boost::algorithm::is_space(), boost::algorithm::token_compress_on); if (!value.empty()) boost::algorithm::split(newTokens, value, boost::algorithm::is_space(), boost::algorithm::token_compress_on); // Merge the two lists std::vector tokens = oldTokens; for (const std::string& newToken : newTokens) { if (newToken[0] == '-') { std::vector::iterator tokenIt = std::find(tokens.begin(), tokens.end(), std::string_view{newToken}.substr(1)); if (tokenIt != tokens.end()) tokens.erase(tokenIt); else { const std::string identifier{ sourceIdentifier ? (" in '" + utf8_from_wstring(sourceIdentifier) + "'") : ""}; LOGWARNING("[ParamNode] Could not remove token " "'%s' from node '%s'%s; not present in " "list nor inherited (possible typo?)", std::string_view{newToken}.substr(1), name, identifier); } } else { if (std::find(oldTokens.begin(), oldTokens.end(), newToken) == oldTokens.end()) tokens.push_back(newToken); } } node.m_Value = boost::algorithm::join(tokens, " "); hasSetValue = true; break; } } } // Add this element as a child node CParamNode& node = m_Childs[name]; if (op != INVALID) { // TODO: Support parsing of data types other than fixed; log warnings in other cases fixed oldval = node.ToFixed(); fixed mod = fixed::FromString(value); switch (op) { case ADD: node.m_Value = (oldval + mod).ToString(); break; case MUL: node.m_Value = oldval.Multiply(mod).ToString(); break; case MUL_ROUND: node.m_Value = fixed::FromInt(oldval.Multiply(mod).ToInt_RoundToNearest()).ToString(); break; default: break; } hasSetValue = true; } if (!hasSetValue && !merging) node.m_Value = value; // We also need to reset node's script val, even if it has no children // or if the attributes change. node.ResetScriptVal(); // For the filtered case ChildrenMap childs; // Recurse through the element's children XERO_ITER_EL(element, child) { node.ApplyLayer(xmb, child, sourceIdentifier); if (filtering) { std::string childname = xmb.GetElementString(child.GetNodeName()); if (node.m_Childs.find(childname) != node.m_Childs.end()) childs[childname] = std::move(node.m_Childs[childname]); } } if (filtering) node.m_Childs.swap(childs); // Add the element's attributes, prefixing names with "@" XERO_ITER_ATTR(element, attr) { // Skip special attributes if (attr.Name == at_replace || attr.Name == at_op || attr.Name == at_merge || attr.Name == at_filtered) continue; // Add any others const char* attrName(xmb.GetAttributeString(attr.Name)); node.m_Childs[CStr("@") + attrName].m_Value = attr.Value; } } const CParamNode& CParamNode::GetOnlyChild() const { if (m_Childs.empty()) return g_NullNode; ENSURE(m_Childs.size() == 1); return m_Childs.begin()->second; } const CParamNode& CParamNode::GetChild(const char* name) const { ChildrenMap::const_iterator it = m_Childs.find(name); if (it == m_Childs.end()) return g_NullNode; return it->second; } bool CParamNode::IsOk() const { return m_IsOk; } const std::wstring CParamNode::ToWString() const { return wstring_from_utf8(m_Value); } const std::string& CParamNode::ToString() const { return m_Value; } const CStrIntern CParamNode::ToUTF8Intern() const { return CStrIntern(m_Value); } int CParamNode::ToInt() const { return std::strtol(m_Value.c_str(), nullptr, 10); } fixed CParamNode::ToFixed() const { return fixed::FromString(m_Value); } float CParamNode::ToFloat() const { return std::strtof(m_Value.c_str(), nullptr); } bool CParamNode::ToBool() const { if (m_Value == "true") return true; else return false; } const CParamNode::ChildrenMap& CParamNode::GetChildren() const { return m_Childs; } std::string CParamNode::EscapeXMLString(const std::string& str) { std::string ret; ret.reserve(str.size()); // TODO: would be nice to check actual v1.0 XML codepoints, // but our UTF8 validation routines are lacking. for (size_t i = 0; i < str.size(); ++i) { char c = str[i]; switch (c) { case '<': ret += "<"; break; case '>': ret += ">"; break; case '&': ret += "&"; break; case '"': ret += """; break; case '\t': ret += " "; break; case '\n': ret += " "; break; case '\r': ret += " "; break; default: ret += c; } } return ret; } std::string CParamNode::ToXMLString() const { std::stringstream strm; ToXMLString(strm); return strm.str(); } void CParamNode::ToXMLString(std::ostream& strm) const { strm << m_Value; ChildrenMap::const_iterator it = m_Childs.begin(); for (; it != m_Childs.end(); ++it) { // Skip attributes here (they were handled when the caller output the tag) if (it->first.length() && it->first[0] == '@') continue; strm << "<" << it->first; // Output the child's attributes first ChildrenMap::const_iterator cit = it->second.m_Childs.begin(); for (; cit != it->second.m_Childs.end(); ++cit) { if (cit->first.length() && cit->first[0] == '@') { std::string attrname (cit->first.begin()+1, cit->first.end()); strm << " " << attrname << "=\"" << EscapeXMLString(cit->second.m_Value) << "\""; } } strm << ">"; it->second.ToXMLString(strm); strm << "first << ">"; } } void CParamNode::ToJSVal(const ScriptRequest& rq, bool cacheValue, JS::MutableHandleValue ret) const { if (cacheValue && m_ScriptVal != NULL) { ret.set(*m_ScriptVal); return; } ConstructJSVal(rq, ret); if (cacheValue) m_ScriptVal.reset(new JS::PersistentRootedValue(rq.cx, ret)); } void CParamNode::ConstructJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret) const { if (m_Childs.empty()) { // Empty node - map to undefined if (m_Value.empty()) { ret.setUndefined(); return; } // Just a string JS::RootedString str(rq.cx, JS_NewStringCopyUTF8Z(rq.cx, JS::ConstUTF8CharsZ(m_Value.data(), m_Value.size()))); str.set(JS_AtomizeAndPinJSString(rq.cx, str)); if (str) { ret.setString(str); return; } // TODO: report error ret.setUndefined(); return; } // Got child nodes - convert this node into a hash-table-style object: JS::RootedObject obj(rq.cx, JS_NewPlainObject(rq.cx)); if (!obj) { ret.setUndefined(); return; // TODO: report error } JS::RootedValue childVal(rq.cx); for (std::map::const_iterator it = m_Childs.begin(); it != m_Childs.end(); ++it) { it->second.ConstructJSVal(rq, &childVal); if (!JS_SetProperty(rq.cx, obj, it->first.c_str(), childVal)) { ret.setUndefined(); return; // TODO: report error } } // If the node has a string too, add that as an extra property if (!m_Value.empty()) { std::u16string text(m_Value.begin(), m_Value.end()); JS::RootedString str(rq.cx, JS_AtomizeAndPinUCStringN(rq.cx, text.c_str(), text.length())); if (!str) { ret.setUndefined(); return; // TODO: report error } JS::RootedValue subChildVal(rq.cx, JS::StringValue(str)); if (!JS_SetProperty(rq.cx, obj, "_string", subChildVal)) { ret.setUndefined(); return; // TODO: report error } } ret.setObject(*obj); } void CParamNode::ResetScriptVal() { m_ScriptVal = NULL; }