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 : }
|