/* Copyright (C) 2021 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 "XMBStorage.h" #include "lib/file/io/write_buffer.h" #include "scriptinterface/ScriptExtraHeaders.h" #include "scriptinterface/ScriptInterface.h" #include #include const char* XMBStorage::HeaderMagicStr = "XMB0"; const char* XMBStorage::UnfinishedHeaderMagicStr = "XMBu"; // Arbitrary version number - change this if we update the code and // need to invalidate old users' caches const u32 XMBStorage::XMBVersion = 4; namespace { class XMBStorageWriter { public: template bool Load(WriteBuffer& writeBuffer, Args&&... args); int GetElementName(const std::string& name) { return GetName(m_ElementSize, m_ElementIDs, name); } int GetAttributeName(const std::string& name) { return GetName(m_AttributeSize, m_AttributeIDs, name); } protected: int GetName(int& totalSize, std::unordered_map& names, const std::string& name) { int nameIdx = totalSize; auto [iterator, inserted] = names.try_emplace(name, nameIdx); if (inserted) totalSize += name.size() + 5; // Add 1 for the null terminator & 4 for the size int. return iterator->second; } void OutputNames(WriteBuffer& writeBuffer, const std::unordered_map& names) const; template bool OutputElements(WriteBuffer&, Args...) { static_assert(sizeof...(Args) != sizeof...(Args), "OutputElements must be specialized."); } int m_ElementSize = 0; int m_AttributeSize = 0; std::unordered_map m_ElementIDs; std::unordered_map m_AttributeIDs; }; // Output text, prefixed by length in bytes (including null-terminator) void WriteStringAndLineNumber(WriteBuffer& writeBuffer, const std::string& text, int lineNumber) { if (text.empty()) { // No text; don't write much writeBuffer.Append("\0\0\0\0", 4); } else { // Write length and line number and null-terminated text u32 nodeLen = u32(4 + text.length() + 1); writeBuffer.Append(&nodeLen, 4); writeBuffer.Append(&lineNumber, 4); writeBuffer.Append((void*)text.c_str(), nodeLen-4); } } template bool XMBStorageWriter::Load(WriteBuffer& writeBuffer, Args&&... args) { // Header writeBuffer.Append(XMBStorage::UnfinishedHeaderMagicStr, 4); // Version writeBuffer.Append(&XMBStorage::XMBVersion, 4); // Filled in below. size_t elementPtr = writeBuffer.Size(); writeBuffer.Append("????????", 8); // Likewise with attributes. size_t attributePtr = writeBuffer.Size(); writeBuffer.Append("????????", 8); if (!OutputElements(writeBuffer, std::forward(args)...)) return false; u32 data = writeBuffer.Size(); writeBuffer.Overwrite(&data, 4, elementPtr); data = m_ElementIDs.size(); writeBuffer.Overwrite(&data, 4, elementPtr + 4); OutputNames(writeBuffer, m_ElementIDs); data = writeBuffer.Size(); writeBuffer.Overwrite(&data, 4, attributePtr); data = m_AttributeIDs.size(); writeBuffer.Overwrite(&data, 4, attributePtr + 4); OutputNames(writeBuffer, m_AttributeIDs); // File is now valid, so insert correct magic string. writeBuffer.Overwrite(XMBStorage::HeaderMagicStr, 4, 0); return true; } void XMBStorageWriter::OutputNames(WriteBuffer& writeBuffer, const std::unordered_map& names) const { std::vector> orderedElements; for (const std::pair& n : names) orderedElements.emplace_back(n); std::sort(orderedElements.begin(), orderedElements.end(), [](const auto& a, const auto&b) { return a.second < b.second; }); for (const std::pair& n : orderedElements) { u32 textLen = (u32)n.first.length() + 1; writeBuffer.Append(&textLen, 4); writeBuffer.Append((void*)n.first.c_str(), textLen); } } class JSNodeData { public: JSNodeData(const ScriptInterface& s) : scriptInterface(s), rq(s) {} bool Setup(XMBStorageWriter& xmb, JS::HandleValue value); bool Output(WriteBuffer& writeBuffer, JS::HandleValue value) const; std::vector> m_Attributes; std::vector>> m_Children; const ScriptInterface& scriptInterface; const ScriptRequest rq; }; template<> bool XMBStorageWriter::OutputElements(WriteBuffer& writeBuffer, JSNodeData& data, const u32& nodeName, JS::HandleValue&& value) { // Set up variables. if (!data.Setup(*this, value)) return false; size_t posLength = writeBuffer.Size(); // Filled in later with the length of the element writeBuffer.Append("????", 4); writeBuffer.Append(&nodeName, 4); u32 attrCount = static_cast(data.m_Attributes.size()); writeBuffer.Append(&attrCount, 4); u32 childCount = data.m_Children.size(); writeBuffer.Append(&childCount, 4); // Filled in later with the offset to the list of child elements size_t posChildrenOffset = writeBuffer.Size(); writeBuffer.Append("????", 4); data.Output(writeBuffer, value); // Output attributes for (const std::pair attr : data.m_Attributes) { writeBuffer.Append(&attr.first, 4); u32 attrLen = u32(attr.second.size())+1; writeBuffer.Append(&attrLen, 4); writeBuffer.Append((void*)attr.second.c_str(), attrLen); } // Go back and fill in the child-element offset u32 childrenOffset = (u32)(writeBuffer.Size() - (posChildrenOffset+4)); writeBuffer.Overwrite(&childrenOffset, 4, posChildrenOffset); // Output all child elements, making a copy since data will be overwritten. std::vector>> children = data.m_Children; for (const std::pair>& child : children) { JS::RootedValue val(data.rq.cx, child.second); if (!OutputElements(writeBuffer, data, child.first, val)) return false; } // Go back and fill in the length u32 length = (u32)(writeBuffer.Size() - posLength); writeBuffer.Overwrite(&length, 4, posLength); return true; } bool JSNodeData::Setup(XMBStorageWriter& xmb, JS::HandleValue value) { m_Attributes.clear(); m_Children.clear(); JSType valType = JS_TypeOfValue(rq.cx, value); if (valType != JSTYPE_OBJECT) return true; std::vector props; if (!scriptInterface.EnumeratePropertyNames(value, true, props)) { LOGERROR("Failed to enumerate component properties."); return false; } for (const std::string& prop : props) { // Special 'value' key. if (prop == "_string") continue; bool attrib = !prop.empty() && prop.front() == '@'; std::string_view name = prop; if (!attrib && !prop.empty() && prop.back() == '@') { size_t idx = prop.substr(0, prop.size()-1).find_last_of('@'); if (idx == std::string::npos) { LOGERROR("Object key name cannot end with an '@' unless it is an index specifier."); return false; } name = std::string_view(prop.c_str(), idx); } else if (attrib) name = std::string_view(prop.c_str()+1, prop.length()-1); JS::RootedValue child(rq.cx); if (!scriptInterface.GetProperty(value, prop.c_str(), &child)) return false; if (attrib) { std::string attrVal; if (!ScriptInterface::FromJSVal(rq, child, attrVal)) { LOGERROR("Attributes must be convertible to string"); return false; } m_Attributes.emplace_back(xmb.GetAttributeName(std::string(name)), attrVal); continue; } bool isArray = false; if (!JS::IsArrayObject(rq.cx, child, &isArray)) return false; if (!isArray) { m_Children.emplace_back(xmb.GetElementName(std::string(name)), child); continue; } // Parse each array object as a child. JS::RootedObject obj(rq.cx); JS_ValueToObject(rq.cx, child, &obj); u32 length; JS::GetArrayLength(rq.cx, obj, &length); for (size_t i = 0; i < length; ++i) { JS::RootedValue arrayChild(rq.cx); scriptInterface.GetPropertyInt(child, i, &arrayChild); m_Children.emplace_back(xmb.GetElementName(std::string(name)), arrayChild); } } return true; } bool JSNodeData::Output(WriteBuffer& writeBuffer, JS::HandleValue value) const { switch (JS_TypeOfValue(rq.cx, value)) { case JSTYPE_UNDEFINED: case JSTYPE_NULL: { writeBuffer.Append("\0\0\0\0", 4); break; } case JSTYPE_OBJECT: { if (!scriptInterface.HasProperty(value, "_string")) { writeBuffer.Append("\0\0\0\0", 4); break; } JS::RootedValue actualValue(rq.cx); if (!scriptInterface.GetProperty(value, "_string", &actualValue)) return false; std::string strVal; if (!ScriptInterface::FromJSVal(rq, actualValue, strVal)) { LOGERROR("'_string' value must be convertible to string"); return false; } WriteStringAndLineNumber(writeBuffer, strVal, 0); break; } case JSTYPE_STRING: case JSTYPE_NUMBER: { std::string strVal; if (!ScriptInterface::FromJSVal(rq, value, strVal)) return false; WriteStringAndLineNumber(writeBuffer, strVal, 0); break; } default: { LOGERROR("Unsupported JS construct when parsing ParamNode"); return false; } } return true; } template<> bool XMBStorageWriter::OutputElements(WriteBuffer& writeBuffer, xmlNodePtr&& node) { // Filled in later with the length of the element size_t posLength = writeBuffer.Size(); writeBuffer.Append("????", 4); u32 name = GetElementName((const char*)node->name); writeBuffer.Append(&name, 4); u32 attrCount = 0; for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) ++attrCount; writeBuffer.Append(&attrCount, 4); u32 childCount = 0; for (xmlNodePtr child = node->children; child; child = child->next) if (child->type == XML_ELEMENT_NODE) ++childCount; writeBuffer.Append(&childCount, 4); // Filled in later with the offset to the list of child elements size_t posChildrenOffset = writeBuffer.Size(); writeBuffer.Append("????", 4); // Trim excess whitespace in the entity's text, while counting // the number of newlines trimmed (so that JS error reporting // can give the correct line number within the script) std::string whitespace = " \t\r\n"; std::string text; for (xmlNodePtr child = node->children; child; child = child->next) { if (child->type == XML_TEXT_NODE) { xmlChar* content = xmlNodeGetContent(child); text += std::string((const char*)content); xmlFree(content); } } u32 linenum = xmlGetLineNo(node); // Find the start of the non-whitespace section size_t first = text.find_first_not_of(whitespace); if (first == text.npos) // Entirely whitespace - easy to handle text = ""; else { // Count the number of \n being cut off, // and add them to the line number std::string trimmed (text.begin(), text.begin()+first); linenum += std::count(trimmed.begin(), trimmed.end(), '\n'); // Find the end of the non-whitespace section, // and trim off everything else size_t last = text.find_last_not_of(whitespace); text = text.substr(first, 1+last-first); } // Output text, prefixed by length in bytes WriteStringAndLineNumber(writeBuffer, text, linenum); // Output attributes for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) { u32 attrName = GetAttributeName((const char*)attr->name); writeBuffer.Append(&attrName, 4); xmlChar* value = xmlNodeGetContent(attr->children); u32 attrLen = u32(xmlStrlen(value)+1); writeBuffer.Append(&attrLen, 4); writeBuffer.Append((void*)value, attrLen); xmlFree(value); } // Go back and fill in the child-element offset u32 childrenOffset = (u32)(writeBuffer.Size() - (posChildrenOffset+4)); writeBuffer.Overwrite(&childrenOffset, 4, posChildrenOffset); // Output all child elements for (xmlNodePtr child = node->children; child; child = child->next) if (child->type == XML_ELEMENT_NODE) OutputElements(writeBuffer, std::move(child)); // Go back and fill in the length u32 length = (u32)(writeBuffer.Size() - posLength); writeBuffer.Overwrite(&length, 4, posLength); return true; } } // anonymous namespace bool XMBStorage::ReadFromFile(const PIVFS& vfs, const VfsPath& filename) { if(vfs->LoadFile(filename, m_Buffer, m_Size) < 0) return false; // if the game crashes during loading, (e.g. due to driver bugs), // it sometimes leaves empty XMB files in the cache. // reporting failure will cause our caller to re-generate the XMB. if (m_Size == 0) return false; ENSURE(m_Size >= 4); // make sure it's at least got the initial header return true; } bool XMBStorage::LoadXMLDoc(const xmlDocPtr doc) { WriteBuffer writeBuffer; XMBStorageWriter writer; if (!writer.Load(writeBuffer, std::move(xmlDocGetRootElement(doc)))) return false; m_Buffer = writeBuffer.Data(); // add a reference m_Size = writeBuffer.Size(); return true; } bool XMBStorage::LoadJSValue(const ScriptInterface& scriptInterface, JS::HandleValue value, const std::string& rootName) { WriteBuffer writeBuffer; XMBStorageWriter writer; const u32 name = writer.GetElementName(rootName); JSNodeData data(scriptInterface); if (!writer.Load(writeBuffer, data, name, std::move(value))) return false; m_Buffer = writeBuffer.Data(); // add a reference m_Size = writeBuffer.Size(); return true; }