LCOV - code coverage report
Current view: top level - source/ps/XMB - XMBStorage.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 203 228 89.0 %
Date: 2023-01-19 00:18:29 Functions: 21 21 100.0 %

          Line data    Source code
       1             : /* Copyright (C) 2022 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 "XMBStorage.h"
      21             : 
      22             : #include "lib/file/io/write_buffer.h"
      23             : #include "lib/file/vfs/vfs.h"
      24             : #include "ps/CLogger.h"
      25             : #include "scriptinterface/Object.h"
      26             : #include "scriptinterface/ScriptConversions.h"
      27             : #include "scriptinterface/ScriptExtraHeaders.h"
      28             : #include "scriptinterface/ScriptInterface.h"
      29             : 
      30             : #include <libxml/parser.h>
      31             : #include <string_view>
      32             : #include <unordered_map>
      33             : 
      34             : const char* XMBStorage::HeaderMagicStr = "XMB0";
      35             : const char* XMBStorage::UnfinishedHeaderMagicStr = "XMBu";
      36             : // Arbitrary version number - change this if we update the code and
      37             : // need to invalidate old users' caches
      38             : const u32 XMBStorage::XMBVersion = 4;
      39             : 
      40             : namespace
      41             : {
      42         222 : class XMBStorageWriter
      43             : {
      44             : public:
      45             :     template<typename ...Args>
      46             :     bool Load(WriteBuffer& writeBuffer, Args&&... args);
      47             : 
      48         457 :     int GetElementName(const std::string& name) { return GetName(m_ElementSize, m_ElementIDs, name); }
      49         134 :     int GetAttributeName(const std::string& name) { return GetName(m_AttributeSize, m_AttributeIDs, name); }
      50             : 
      51             : protected:
      52         591 :     int GetName(int& totalSize, std::unordered_map<std::string, int>& names, const std::string& name)
      53             :     {
      54         591 :         int nameIdx = totalSize;
      55         591 :         auto [iterator, inserted] = names.try_emplace(name, nameIdx);
      56         591 :         if (inserted)
      57         517 :             totalSize += name.size() + 5; // Add 1 for the null terminator & 4 for the size int.
      58         591 :         return iterator->second;
      59             :     }
      60             : 
      61             :     void OutputNames(WriteBuffer& writeBuffer, const std::unordered_map<std::string, int>& names) const;
      62             : 
      63             :     template<typename ...Args>
      64             :     bool OutputElements(WriteBuffer&, Args...)
      65             :     {
      66             :         static_assert(sizeof...(Args) != sizeof...(Args), "OutputElements must be specialized.");
      67             :         return false;
      68             :     }
      69             : 
      70             :     int m_ElementSize = 0;
      71             :     int m_AttributeSize = 0;
      72             :     std::unordered_map<std::string, int> m_ElementIDs;
      73             :     std::unordered_map<std::string, int> m_AttributeIDs;
      74             : };
      75             : 
      76             : // Output text, prefixed by length in bytes (including null-terminator)
      77         453 : void WriteStringAndLineNumber(WriteBuffer& writeBuffer, const std::string& text, int lineNumber)
      78             : {
      79         453 :     if (text.empty())
      80             :     {
      81             :         // No text; don't write much
      82         252 :         writeBuffer.Append("\0\0\0\0", 4);
      83             :     }
      84             :     else
      85             :     {
      86             :         // Write length and line number and null-terminated text
      87         201 :         u32 nodeLen = u32(4 + text.length() + 1);
      88         201 :         writeBuffer.Append(&nodeLen, 4);
      89         201 :         writeBuffer.Append(&lineNumber, 4);
      90         201 :         writeBuffer.Append((void*)text.c_str(), nodeLen-4);
      91             :     }
      92         453 : }
      93             : 
      94             : template<typename ...Args>
      95         111 : bool XMBStorageWriter::Load(WriteBuffer& writeBuffer, Args&&... args)
      96             : {
      97             :     // Header
      98         111 :     writeBuffer.Append(XMBStorage::UnfinishedHeaderMagicStr, 4);
      99             :     // Version
     100         111 :     writeBuffer.Append(&XMBStorage::XMBVersion, 4);
     101             : 
     102             :     // Filled in below.
     103         111 :     size_t elementPtr = writeBuffer.Size();
     104         111 :     writeBuffer.Append("????????", 8);
     105             :     // Likewise with attributes.
     106         111 :     size_t attributePtr = writeBuffer.Size();
     107         111 :     writeBuffer.Append("????????", 8);
     108             : 
     109         111 :     if (!OutputElements<Args&&...>(writeBuffer, std::forward<Args>(args)...))
     110           0 :         return false;
     111             : 
     112         111 :     u32 data = writeBuffer.Size();
     113         111 :     writeBuffer.Overwrite(&data, 4, elementPtr);
     114         111 :     data = m_ElementIDs.size();
     115         111 :     writeBuffer.Overwrite(&data, 4, elementPtr + 4);
     116         111 :     OutputNames(writeBuffer, m_ElementIDs);
     117             : 
     118         111 :     data = writeBuffer.Size();
     119         111 :     writeBuffer.Overwrite(&data, 4, attributePtr);
     120         111 :     data = m_AttributeIDs.size();
     121         111 :     writeBuffer.Overwrite(&data, 4, attributePtr + 4);
     122         111 :     OutputNames(writeBuffer, m_AttributeIDs);
     123             : 
     124             :     // File is now valid, so insert correct magic string.
     125         111 :     writeBuffer.Overwrite(XMBStorage::HeaderMagicStr, 4, 0);
     126             : 
     127         111 :     return true;
     128             : }
     129             : 
     130         222 : void XMBStorageWriter::OutputNames(WriteBuffer& writeBuffer, const std::unordered_map<std::string, int>& names) const
     131             : {
     132         444 :     std::vector<std::pair<std::string, int>> orderedElements;
     133         739 :     for (const std::pair<const std::string, int>& n : names)
     134         517 :         orderedElements.emplace_back(n);
     135        1343 :     std::sort(orderedElements.begin(), orderedElements.end(), [](const auto& a, const auto&b) { return a.second < b.second; });
     136         739 :     for (const std::pair<std::string, int>& n : orderedElements)
     137             :     {
     138         517 :         u32 textLen = (u32)n.first.length() + 1;
     139         517 :         writeBuffer.Append(&textLen, 4);
     140         517 :         writeBuffer.Append((void*)n.first.c_str(), textLen);
     141             :     }
     142         222 : }
     143             : 
     144           4 : class JSNodeData
     145             : {
     146             : public:
     147           4 :     JSNodeData(const ScriptInterface& s) : scriptInterface(s), rq(s) {}
     148             : 
     149             :     bool Setup(XMBStorageWriter& xmb, JS::HandleValue value);
     150             :     bool Output(WriteBuffer& writeBuffer, JS::HandleValue value) const;
     151             : 
     152             :     std::vector<std::pair<u32, std::string>> m_Attributes;
     153             :     std::vector<std::pair<u32, JS::Heap<JS::Value>>> m_Children;
     154             : 
     155             :     const ScriptInterface& scriptInterface;
     156             :     const ScriptRequest rq;
     157             : };
     158             : 
     159             : template<>
     160          16 : bool XMBStorageWriter::OutputElements<JSNodeData&, const u32&, JS::HandleValue&&>(WriteBuffer& writeBuffer, JSNodeData& data, const u32& nodeName, JS::HandleValue&& value)
     161             : {
     162             :     // Set up variables.
     163          16 :     if (!data.Setup(*this, value))
     164           0 :         return false;
     165             : 
     166          16 :     size_t posLength = writeBuffer.Size();
     167             :     // Filled in later with the length of the element
     168          16 :     writeBuffer.Append("????", 4);
     169             : 
     170          16 :     writeBuffer.Append(&nodeName, 4);
     171             : 
     172          16 :     u32 attrCount = static_cast<u32>(data.m_Attributes.size());
     173          16 :     writeBuffer.Append(&attrCount, 4);
     174             : 
     175          16 :     u32 childCount = data.m_Children.size();
     176          16 :     writeBuffer.Append(&childCount, 4);
     177             : 
     178             :     // Filled in later with the offset to the list of child elements
     179          16 :     size_t posChildrenOffset = writeBuffer.Size();
     180          16 :     writeBuffer.Append("????", 4);
     181             : 
     182          16 :     data.Output(writeBuffer, value);
     183             : 
     184             :     // Output attributes
     185          18 :     for (const std::pair<const u32, std::string> attr : data.m_Attributes)
     186             :     {
     187           2 :         writeBuffer.Append(&attr.first, 4);
     188           2 :         u32 attrLen = u32(attr.second.size())+1;
     189           2 :         writeBuffer.Append(&attrLen, 4);
     190           2 :         writeBuffer.Append((void*)attr.second.c_str(), attrLen);
     191             :     }
     192             : 
     193             :     // Go back and fill in the child-element offset
     194          16 :     u32 childrenOffset = (u32)(writeBuffer.Size() - (posChildrenOffset+4));
     195          16 :     writeBuffer.Overwrite(&childrenOffset, 4, posChildrenOffset);
     196             : 
     197             :     // Output all child elements, making a copy since data will be overwritten.
     198          32 :     std::vector<std::pair<u32, JS::Heap<JS::Value>>> children = data.m_Children;
     199          28 :     for (const std::pair<u32, JS::Heap<JS::Value>>& child : children)
     200             :     {
     201          24 :         JS::RootedValue val(data.rq.cx, child.second);
     202          12 :         if (!OutputElements<JSNodeData&, const u32&, JS::HandleValue&&>(writeBuffer, data, child.first, val))
     203           0 :             return false;
     204             :     }
     205             : 
     206             :     // Go back and fill in the length
     207          16 :     u32 length = (u32)(writeBuffer.Size() - posLength);
     208          16 :     writeBuffer.Overwrite(&length, 4, posLength);
     209             : 
     210          16 :     return true;
     211             : }
     212             : 
     213          16 : bool JSNodeData::Setup(XMBStorageWriter& xmb, JS::HandleValue value)
     214             : {
     215          16 :     m_Attributes.clear();
     216          16 :     m_Children.clear();
     217          16 :     JSType valType = JS_TypeOfValue(rq.cx, value);
     218          16 :     if (valType != JSTYPE_OBJECT)
     219           7 :         return true;
     220             : 
     221          18 :     std::vector<std::string> props;
     222           9 :     if (!Script::EnumeratePropertyNames(rq, value, true, props))
     223             :     {
     224           0 :         LOGERROR("Failed to enumerate component properties.");
     225           0 :         return false;
     226             :     }
     227             : 
     228          26 :     for (const std::string& prop : props)
     229             :     {
     230             :         // Special 'value' key.
     231          17 :         if (prop == "_string")
     232          20 :             continue;
     233             : 
     234          12 :         bool attrib = !prop.empty() && prop.front() == '@';
     235             : 
     236          12 :         std::string_view name = prop;
     237          12 :         if (!attrib && !prop.empty() && prop.back() == '@')
     238             :         {
     239           8 :             const size_t idx = std::string_view{prop}.substr(0, prop.size() - 1)
     240           4 :                 .find_last_of('@');
     241           4 :             if (idx == std::string::npos)
     242             :             {
     243           0 :                 LOGERROR("Object key name cannot end with an '@' unless it is an index specifier.");
     244           0 :                 return false;
     245             :             }
     246           4 :             name = std::string_view(prop.c_str(), idx);
     247             :         }
     248           8 :         else if (attrib)
     249           2 :             name = std::string_view(prop.c_str()+1, prop.length()-1);
     250             : 
     251          14 :         JS::RootedValue child(rq.cx);
     252          12 :         if (!Script::GetProperty(rq, value, prop.c_str(), &child))
     253           0 :             return false;
     254             : 
     255          12 :         if (attrib)
     256             :         {
     257           2 :             std::string attrVal;
     258           2 :             if (!Script::FromJSVal(rq, child, attrVal))
     259             :             {
     260           0 :                 LOGERROR("Attributes must be convertible to string");
     261           0 :                 return false;
     262             :             }
     263           2 :             m_Attributes.emplace_back(xmb.GetAttributeName(std::string(name)), attrVal);
     264           2 :             continue;
     265             :         }
     266             : 
     267          10 :         bool isArray = false;
     268          10 :         if (!JS::IsArrayObject(rq.cx, child, &isArray))
     269           0 :             return false;
     270          18 :         if (!isArray)
     271             :         {
     272           8 :             m_Children.emplace_back(xmb.GetElementName(std::string(name)), child);
     273           8 :             continue;
     274             :         }
     275             : 
     276             :         // Parse each array object as a child.
     277           4 :         JS::RootedObject obj(rq.cx);
     278           2 :         JS_ValueToObject(rq.cx, child, &obj);
     279             :         u32 length;
     280           2 :         JS::GetArrayLength(rq.cx, obj, &length);
     281           6 :         for (size_t i = 0; i < length; ++i)
     282             :         {
     283           8 :             JS::RootedValue arrayChild(rq.cx);
     284           4 :             Script::GetPropertyInt(rq, child, i, &arrayChild);
     285           4 :             m_Children.emplace_back(xmb.GetElementName(std::string(name)), arrayChild);
     286             :         }
     287             :     }
     288           9 :     return true;
     289             : }
     290             : 
     291          16 : bool JSNodeData::Output(WriteBuffer& writeBuffer, JS::HandleValue value) const
     292             : {
     293          16 :     switch (JS_TypeOfValue(rq.cx, value))
     294             :     {
     295           0 :         case JSTYPE_UNDEFINED:
     296             :         case JSTYPE_NULL:
     297             :         {
     298           0 :             writeBuffer.Append("\0\0\0\0", 4);
     299           0 :             break;
     300             :         }
     301           9 :         case JSTYPE_OBJECT:
     302             :         {
     303           9 :             if (!Script::HasProperty(rq, value, "_string"))
     304             :             {
     305           4 :                 writeBuffer.Append("\0\0\0\0", 4);
     306          13 :                 break;
     307             :             }
     308           5 :             JS::RootedValue actualValue(rq.cx);
     309           5 :             if (!Script::GetProperty(rq, value, "_string", &actualValue))
     310           0 :                 return false;
     311           5 :             std::string strVal;
     312           5 :             if (!Script::FromJSVal(rq, actualValue, strVal))
     313             :             {
     314           0 :                 LOGERROR("'_string' value must be convertible to string");
     315           0 :                 return false;
     316             :             }
     317           5 :             WriteStringAndLineNumber(writeBuffer, strVal, 0);
     318           5 :             break;
     319             :         }
     320           7 :         case JSTYPE_STRING:
     321             :         case JSTYPE_NUMBER:
     322             :         {
     323           7 :             std::string strVal;
     324           7 :             if (!Script::FromJSVal(rq, value, strVal))
     325           0 :                 return false;
     326             : 
     327           7 :             WriteStringAndLineNumber(writeBuffer, strVal, 0);
     328           7 :             break;
     329             :         }
     330           0 :         default:
     331             :         {
     332           0 :             LOGERROR("Unsupported JS construct when parsing ParamNode");
     333           0 :             return false;
     334             :         }
     335             :     }
     336          16 :     return true;
     337             : }
     338             : 
     339             : template<>
     340         441 : bool XMBStorageWriter::OutputElements<xmlNodePtr&&>(WriteBuffer& writeBuffer, xmlNodePtr&& node)
     341             : {
     342             :     // Filled in later with the length of the element
     343         441 :     size_t posLength = writeBuffer.Size();
     344         441 :     writeBuffer.Append("????", 4);
     345             : 
     346         441 :     u32 name = GetElementName((const char*)node->name);
     347         441 :     writeBuffer.Append(&name, 4);
     348             : 
     349         441 :     u32 attrCount = 0;
     350         573 :     for (xmlAttrPtr attr = node->properties; attr; attr = attr->next)
     351         132 :         ++attrCount;
     352         441 :     writeBuffer.Append(&attrCount, 4);
     353             : 
     354         441 :     u32 childCount = 0;
     355        1334 :     for (xmlNodePtr child = node->children; child; child = child->next)
     356         893 :         if (child->type == XML_ELEMENT_NODE)
     357         334 :             ++childCount;
     358         441 :     writeBuffer.Append(&childCount, 4);
     359             : 
     360             :     // Filled in later with the offset to the list of child elements
     361         441 :     size_t posChildrenOffset = writeBuffer.Size();
     362         441 :     writeBuffer.Append("????", 4);
     363             : 
     364             : 
     365             :     // Trim excess whitespace in the entity's text, while counting
     366             :     // the number of newlines trimmed (so that JS error reporting
     367             :     // can give the correct line number within the script)
     368             : 
     369         882 :     std::string whitespace = " \t\r\n";
     370         882 :     std::string text;
     371        1334 :     for (xmlNodePtr child = node->children; child; child = child->next)
     372             :     {
     373         893 :         if (child->type == XML_TEXT_NODE)
     374             :         {
     375         538 :             xmlChar* content = xmlNodeGetContent(child);
     376         538 :             text += std::string((const char*)content);
     377         538 :             xmlFree(content);
     378             :         }
     379             :     }
     380             : 
     381         441 :     u32 linenum = xmlGetLineNo(node);
     382             : 
     383             :     // Find the start of the non-whitespace section
     384         441 :     size_t first = text.find_first_not_of(whitespace);
     385             : 
     386         441 :     if (first == text.npos)
     387             :         // Entirely whitespace - easy to handle
     388         252 :         text = "";
     389             : 
     390             :     else
     391             :     {
     392             :         // Count the number of \n being cut off,
     393             :         // and add them to the line number
     394         378 :         std::string trimmed (text.begin(), text.begin()+first);
     395         189 :         linenum += std::count(trimmed.begin(), trimmed.end(), '\n');
     396             : 
     397             :         // Find the end of the non-whitespace section,
     398             :         // and trim off everything else
     399         189 :         size_t last = text.find_last_not_of(whitespace);
     400         189 :         text = text.substr(first, 1+last-first);
     401             :     }
     402             : 
     403             : 
     404             :     // Output text, prefixed by length in bytes
     405         441 :     WriteStringAndLineNumber(writeBuffer, text, linenum);
     406             : 
     407             :     // Output attributes
     408         573 :     for (xmlAttrPtr attr = node->properties; attr; attr = attr->next)
     409             :     {
     410         132 :         u32 attrName = GetAttributeName((const char*)attr->name);
     411         132 :         writeBuffer.Append(&attrName, 4);
     412             : 
     413         132 :         xmlChar* value = xmlNodeGetContent(attr->children);
     414         132 :         u32 attrLen = u32(xmlStrlen(value)+1);
     415         132 :         writeBuffer.Append(&attrLen, 4);
     416         132 :         writeBuffer.Append((void*)value, attrLen);
     417         132 :         xmlFree(value);
     418             :     }
     419             : 
     420             :     // Go back and fill in the child-element offset
     421         441 :     u32 childrenOffset = (u32)(writeBuffer.Size() - (posChildrenOffset+4));
     422         441 :     writeBuffer.Overwrite(&childrenOffset, 4, posChildrenOffset);
     423             : 
     424             :     // Output all child elements
     425        1334 :     for (xmlNodePtr child = node->children; child; child = child->next)
     426         893 :         if (child->type == XML_ELEMENT_NODE)
     427         334 :             OutputElements<xmlNodePtr&&>(writeBuffer, std::move(child));
     428             : 
     429             :     // Go back and fill in the length
     430         441 :     u32 length = (u32)(writeBuffer.Size() - posLength);
     431         441 :     writeBuffer.Overwrite(&length, 4, posLength);
     432             : 
     433         882 :     return true;
     434             : }
     435             : } // anonymous namespace
     436             : 
     437         314 : bool XMBStorage::ReadFromFile(const PIVFS& vfs, const VfsPath& filename)
     438             : {
     439         314 :     if(vfs->LoadFile(filename, m_Buffer, m_Size) < 0)
     440           0 :         return false;
     441             :     // if the game crashes during loading, (e.g. due to driver bugs),
     442             :     // it sometimes leaves empty XMB files in the cache.
     443             :     // reporting failure will cause our caller to re-generate the XMB.
     444         314 :     if (m_Size == 0)
     445           0 :         return false;
     446         314 :     ENSURE(m_Size >= 4); // make sure it's at least got the initial header
     447         314 :     return true;
     448             : }
     449             : 
     450         107 : bool XMBStorage::LoadXMLDoc(const xmlDocPtr doc)
     451             : {
     452         214 :     WriteBuffer writeBuffer;
     453             : 
     454         214 :     XMBStorageWriter writer;
     455         107 :     if (!writer.Load(writeBuffer, std::move(xmlDocGetRootElement(doc))))
     456           0 :         return false;
     457             : 
     458         107 :     m_Buffer = writeBuffer.Data(); // add a reference
     459         107 :     m_Size = writeBuffer.Size();
     460         107 :     return true;
     461             : }
     462             : 
     463           4 : bool XMBStorage::LoadJSValue(const ScriptInterface& scriptInterface, JS::HandleValue value, const std::string& rootName)
     464             : {
     465           8 :     WriteBuffer writeBuffer;
     466             : 
     467           8 :     XMBStorageWriter writer;
     468           4 :     const u32 name = writer.GetElementName(rootName);
     469           8 :     JSNodeData data(scriptInterface);
     470           4 :     if (!writer.Load(writeBuffer, data, name, std::move(value)))
     471           0 :         return false;
     472             : 
     473           4 :     m_Buffer = writeBuffer.Data(); // add a reference
     474           4 :     m_Size = writeBuffer.Size();
     475           4 :     return true;
     476           3 : }

Generated by: LCOV version 1.13