Files
2026-02-02 04:50:13 +01:00

551 lines
13 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "ultima/shared/conf/xml_node.h"
#include "ultima/shared/conf/xml_tree.h"
#include "common/file.h"
namespace Ultima {
namespace Shared {
XMLNode::~XMLNode() {
for (auto *node : _nodeList) {
delete node;
}
}
const Common::String &XMLNode::reference(const Common::String &h, bool &exists) {
if (h.find('/') == Common::String::npos) {
// Must refer to me.
if (_id == h) {
exists = true;
return _text;
}
} else {
// Otherwise we want to split the Common::String at the first /
// then locate the branch to walk, and pass the rest
// down.
Common::String k;
k = h.substr(h.find('/') + 1);
Common::String k2 = k.substr(0, k.find('/'));
for (auto *node : _nodeList) {
if (node->_id == k2)
return node->reference(k, exists);
}
}
exists = false;
return _emptyString;
}
const XMLNode *XMLNode::subtree(const Common::String &h) const {
if (h.find('/') == Common::String::npos) {
// Must refer to me.
if (_id.equalsIgnoreCase(h))
return this;
} else {
// Otherwise we want to split the Common::String at the first /
// then locate the branch to walk, and pass the rest
// down.
Common::String k;
k = h.substr(h.find('/') + 1);
Common::String k2 = k.substr(0, k.find('/'));
for (auto *node : _nodeList) {
if (node->_id.equalsIgnoreCase(k2)) {
return node->subtree(k);
}
}
}
return nullptr;
}
Common::String XMLNode::dump(int depth) {
Common::String s;
for (int i = 0; i < depth; ++i)
s += ' ';
s += "<";
s += _id;
s += ">";
if (_id[_id.size() - 1] != '/') {
if (_nodeList.empty() == false)
s += "\n";
for (auto *node : _nodeList) {
s += node->dump(depth + 1);
}
if (!_text.empty()) {
//s += Common::String(depth,' ');
s += encodeEntity(_text);
}
if (_id[0] == '?') {
return s;
}
//if(_content.size())
// s += "\n";
if (!_noClose) {
if (_text.empty()) {
for (int i = 0; i < depth; ++i)
s += ' ';
}
s += "</";
s += closeTag(_id);
s += ">\n";
}
}
return s;
}
void XMLNode::xmlAssign(const Common::String &key, const Common::String &value) {
if (key.find('/') == Common::String::npos) {
// Must refer to me.
if (_id == key)
_text = value;
else
error("Walking the XML tree failed to create a final node.");
return;
}
Common::String k;
k = key.substr(key.find('/') + 1);
Common::String k2 = k.substr(0, k.find('/'));
for (auto *node : _nodeList) {
if (node->_id == k2) {
node->xmlAssign(k, value);
return;
}
}
// No match, so create a new node and do recursion
XMLNode *t = new XMLNode(_tree);
t->_parent = this;
t->_id = k2;
_nodeList.push_back(t);
(*t).xmlAssign(k, value);
}
void XMLNode::listKeys(const Common::String &key, Common::Array<Common::String> &vs,
bool longformat) const {
Common::String s(key);
s += "/";
for (auto *node : _nodeList) {
if (!longformat)
vs.push_back(node->_id);
else
vs.push_back(s + node->_id);
}
}
Common::String XMLNode::encodeEntity(const Common::String &s) {
Common::String ret;
for (const auto &c : s) {
switch (c) {
case '<':
ret += "&lt;";
break;
case '>':
ret += "&gt;";
break;
case '"':
ret += "&quot;";
break;
case '\'':
ret += "&apos;";
break;
case '&':
ret += "&amp;";
break;
default:
ret += c;
}
}
return ret;
}
static Common::String decode_entity(const Common::String &s, size_t &pos) {
// size_t old_pos = pos;
size_t entityNameLen = s.findFirstOf("; \t\r\n", pos) - pos - 1;
/* Call me paranoid... but I don't think having an end-of-line or similar
inside a &...; expression is 'good', valid though it may be. */
assert(s[pos + entityNameLen + 1] == ';');
Common::String entity_name = s.substr(pos + 1, entityNameLen);
pos += entityNameLen + 2;
// Std::cout << "DECODE: " << entity_name << endl;
if (entity_name == "amp")
return Common::String("&");
else if (entity_name == "apos")
return Common::String("'");
else if (entity_name == "quot")
return Common::String("\"");
else if (entity_name == "lt")
return Common::String("<");
else if (entity_name == "gt")
return Common::String(">");
else if (entity_name.hasPrefix("#")) {
entity_name.deleteChar(0);
if (entity_name.hasPrefix("x")) {
uint tmp = 0;
int read = sscanf(entity_name.c_str() + 1, "%xh", &tmp);
if (read < 1)
error("strToInt failed on string \"%s\"", entity_name.c_str());
return Common::String((char)tmp);
} else {
uint tmp = atol(entity_name.c_str());
return Common::String((char)tmp);
}
} else {
error("Invalid xml encoded entity - %s", entity_name.c_str());
}
}
XMLNode *XMLNode::xmlParseDoc(XMLTree *tree, const Common::String &s) {
Common::String sbuf(s);
size_t nn = 0;
bool parsedXmlElement = false, parsedDocType = false;
XMLNode *node = nullptr, *child = nullptr;
for (;;) {
while (nn < s.size() && Common::isSpace(s[nn]))
++nn;
if (nn >= s.size())
return node;
if (s[nn] != '<') {
warning("expected '<' while reading config file, found %c\n", s[nn]);
return nullptr;
}
++nn;
if (nn < s.size() && s[nn] == '?') {
assert(!parsedXmlElement);
parsedXmlElement = true;
nn = s.findFirstOf('>', nn);
} else if (nn < s.size() && s.substr(nn, 8).equalsIgnoreCase("!doctype")) {
assert(!parsedDocType);
parsedDocType = true;
parseDocTypeElement(s, nn);
} else {
--nn;
child = xmlParse(tree, sbuf, nn);
if (child) {
if (node)
error("Invalid multiple xml nodes at same level");
node = child;
}
continue;
}
// If this point was reached, we just skipped ?xml or doctype element
++nn;
}
return node;
}
void XMLNode::parseDocTypeElement(const Common::String &s, size_t &nn) {
nn = s.findFirstOf(">[", nn);
if (nn == Common::String::npos)
// No ending tag
return;
if (s[nn] == '[') {
// Square bracketed area
nn = s.findFirstOf(']', nn) + 1;
}
if (nn >= s.size() || s[nn] != '>')
nn = Common::String::npos;
}
XMLNode *XMLNode::xmlParse(XMLTree *tree, const Common::String &s, size_t &pos) {
bool inTag = false, isSelfEnding = false;
XMLNode *node = nullptr, *child = nullptr;
Common::String nodeText, plainText;
while (pos < s.size()) {
if (inTag && nodeText.hasPrefix("!--") && (s[pos] != '>' || !nodeText.hasSuffix("--"))) {
// It's a > within the comment, but not the terminator
nodeText += s[pos];
++pos;
continue;
}
switch (s[pos]) {
case '<':
// Start of tag
assert(!inTag);
trim(plainText);
if (!plainText.empty()) {
// Plain text, return it
child = new XMLNode(tree);
child->_text = plainText;
return child;
}
// New tag?
if (s[pos + 1] == '/') {
// No. It's a close tag. Move beyond it
while (s[pos] != '>')
pos++;
++pos;
// Return nullptr as indication that the end was reached
return nullptr;
}
inTag = true;
++pos;
break;
case '>':
// End of tag
isSelfEnding = nodeText.hasSuffix("/");
++pos;
if (isSelfEnding) {
nodeText.deleteLastChar();
} else if (nodeText.hasPrefix("!--")) {
// Comment element that can be ignored
inTag = false;
nodeText.clear();
plainText.clear();
break;
}
// Create a node for the tag, and parse it's attributes, if any
node = new XMLNode(tree);
node->parseNodeText(nodeText);
if (!isSelfEnding) {
// Iterate through parsing and adding sub-elements
while ((child = xmlParse(tree, s, pos)) != nullptr) {
child->_parent = node;
node->_nodeList.push_back(child);
}
} else if (node->_id.equalsIgnoreCase("xi:include")) {
// Element is a placeholder for inclusion of a secondary XML file
Common::String fname = node->_attributes["href"];
delete node;
node = xmlParseFile(tree, Common::Path(fname, '/'));
}
return node;
case '&':
if (inTag)
nodeText += decode_entity(s, pos);
else
plainText += decode_entity(s, pos);
break;
default:
if (inTag)
nodeText += s[pos++];
else
plainText += s[pos++];
break;
}
}
return node;
}
void XMLNode::parseNodeText(const Common::String &nodeText) {
size_t firstSpace = nodeText.findFirstOf(' ');
if (firstSpace == Common::String::npos) {
// The entire text is the id
_id = nodeText;
return;
}
// Set the Id and get out the remaining attributes section, if any
_id = Common::String(nodeText.c_str(), firstSpace);
Common::String attr(nodeText.c_str() + firstSpace);
for (;;) {
// Skip any spaces
while (!attr.empty() && Common::isSpace(attr[0]))
attr.deleteChar(0);
if (attr.empty())
return;
// Find the equals after the attribute name
size_t equalsPos = attr.findFirstOf('=');
if (equalsPos == Common::String::npos)
return;
// Get the name, and find the quotes start
Common::String name = Common::String(attr.c_str(), equalsPos);
++equalsPos;
while (equalsPos < attr.size() && Common::isSpace(attr[equalsPos]))
++equalsPos;
if (attr[equalsPos] == '\'' && attr[equalsPos] != '"')
return;
// Find the end of the attribute
size_t attrEnd = attr.findFirstOf(attr[equalsPos], equalsPos + 1);
if (attrEnd == Common::String::npos)
return;
// Add the parsed attribute
_attributes[name] = Common::String(attr.c_str() + equalsPos + 1, attr.c_str() + attrEnd);
// Remove the parsed attribute
attr = Common::String(attr.c_str() + attrEnd + 1);
}
}
XMLNode *XMLNode::xmlParseFile(XMLTree *tree, const Common::Path &fname) {
const Common::Path rootFile = tree->_filename;
Common::Path filename = rootFile.getParent().join(fname);
Common::File f;
if (!f.open(filename))
error("Could not open xml file - %s", filename.toString().c_str());
// Read in the file contents
char *buf = new char[f.size() + 1];
f.read(buf, f.size());
buf[f.size()] = '\0';
Common::String text(buf, buf + f.size());
delete[] buf;
f.close();
// Parse the sub-xml
XMLNode *result = xmlParseDoc(tree, text);
if (!result)
error("Error passing xml - %s", fname.toString().c_str());
return result;
}
bool XMLNode::searchPairs(KeyTypeList &ktl, const Common::String &basekey,
const Common::String &currkey, const unsigned int pos) {
/* If our 'current key' is longer then the key we're serching for
we've obviously gone too deep in this branch, and we won't find
it here. */
if ((currkey.size() <= basekey.size()) && (_id[0] != '!')) {
/* If we've found it, return every key->value pair under this key,
then return true, since we've found the key we were looking for.*/
if (basekey == currkey + _id) {
for (auto *node : _nodeList) {
if (node->_id[0] != '!')
node->selectPairs(ktl, "");
}
return true;
}
/* Else, keep searching for the key under it's subnodes */
else {
for (auto *node : _nodeList) {
if (node->searchPairs(ktl, basekey, currkey + _id + '/', pos))
return true;
}
}
}
return false;
}
/* Just adds every key->value pair under the this node to the ktl */
void XMLNode::selectPairs(KeyTypeList &ktl, const Common::String &currkey) {
ktl.push_back(KeyType(currkey + _id, currkey));
for (auto *node : _nodeList) {
node->selectPairs(ktl, currkey + _id + '/');
}
}
XMLNode *XMLNode::getPrior() const {
const Common::Array<XMLNode *> &siblings = _parent->_nodeList;
for (uint idx = 0; idx < siblings.size(); ++idx) {
if (siblings[idx] == this)
return (idx > 0) ? siblings[idx - 1] : nullptr;
}
return nullptr;
}
XMLNode *XMLNode::getNext() const {
const Common::Array<XMLNode *> &siblings = _parent->_nodeList;
for (uint idx = 0; idx < siblings.size(); ++idx) {
if (siblings[idx] == this)
return (idx < (siblings.size() - 1)) ? siblings[idx + 1] : nullptr;
}
return nullptr;
}
void XMLNode::freeDoc() {
delete _tree;
}
void XMLNode::trim(Common::String &s) {
// Convert any CRLF to just LF
size_t pos;
while ((pos = s.find("\r\n")) != Common::String::npos)
s.deleteChar(pos);
// Then check if the string is entirely whitespace
bool hasContent = false;
for (uint idx = 0; idx < s.size() && !hasContent; ++idx)
hasContent = !Common::isSpace(s[idx]);
if (!hasContent) {
s = "";
return;
}
// Remove any spaces from the very start of the string and following
// any linefeeds. This handles any indented text within the xml
for (size_t startPos = 0; startPos != Common::String::npos;
startPos = s.findFirstOf('\n', startPos + 1)) {
pos = (startPos == 0) ? 0 : startPos + 1;
while (pos < s.size() && s[pos] == ' ')
s.deleteChar(pos);
}
}
Common::String XMLNode::closeTag(const Common::String &s) {
if (s.find(" ") == Common::String::npos)
return s;
return s.substr(0, s.find(" "));
}
} // End of namespace Shared
} // End of namespace Ultima