mirror of
https://github.com/libretro/scummvm.git
synced 2024-12-03 15:41:41 +00:00
523 lines
11 KiB
C++
523 lines
11 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 2
|
|
* 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, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
*/
|
|
|
|
#include "common/xmlparser.h"
|
|
#include "common/archive.h"
|
|
#include "common/fs.h"
|
|
#include "common/memstream.h"
|
|
#include "common/system.h"
|
|
|
|
namespace Common {
|
|
|
|
XMLParser::~XMLParser() {
|
|
while (!_activeKey.empty())
|
|
freeNode(_activeKey.pop());
|
|
|
|
delete _XMLkeys;
|
|
delete _stream;
|
|
|
|
for (List<XMLKeyLayout *>::iterator i = _layoutList.begin();
|
|
i != _layoutList.end(); ++i)
|
|
delete *i;
|
|
|
|
_layoutList.clear();
|
|
}
|
|
|
|
bool XMLParser::loadFile(const String &filename) {
|
|
_stream = SearchMan.createReadStreamForMember(filename);
|
|
if (!_stream)
|
|
return false;
|
|
|
|
_fileName = filename;
|
|
return true;
|
|
}
|
|
|
|
bool XMLParser::loadFile(const FSNode &node) {
|
|
_stream = node.createReadStream();
|
|
if (!_stream)
|
|
return false;
|
|
|
|
_fileName = node.getName();
|
|
return true;
|
|
}
|
|
|
|
bool XMLParser::loadBuffer(const byte *buffer, uint32 size, DisposeAfterUse::Flag disposable) {
|
|
_stream = new MemoryReadStream(buffer, size, disposable);
|
|
_fileName = "Memory Stream";
|
|
return true;
|
|
}
|
|
|
|
bool XMLParser::loadStream(SeekableReadStream *stream) {
|
|
_stream = stream;
|
|
_fileName = "File Stream";
|
|
return _stream != nullptr;
|
|
}
|
|
|
|
void XMLParser::close() {
|
|
delete _stream;
|
|
_stream = nullptr;
|
|
}
|
|
|
|
bool XMLParser::parserError(const String &errStr) {
|
|
_state = kParserError;
|
|
|
|
const int startPosition = _stream->pos();
|
|
int currentPosition = startPosition;
|
|
int lineCount = 1;
|
|
char c = 0;
|
|
|
|
_stream->seek(0, SEEK_SET);
|
|
|
|
while (currentPosition--) {
|
|
c = _stream->readByte();
|
|
|
|
if (c == '\n' || c == '\r')
|
|
lineCount++;
|
|
}
|
|
|
|
assert(_stream->pos() == startPosition);
|
|
currentPosition = startPosition;
|
|
|
|
Common::String errorMessage = Common::String::format("\n File <%s>, line %d:\n", _fileName.c_str(), lineCount);
|
|
|
|
if (startPosition > 1) {
|
|
int keyOpening = 0;
|
|
int keyClosing = 0;
|
|
|
|
while (currentPosition-- && keyOpening == 0) {
|
|
_stream->seek(-2, SEEK_CUR);
|
|
c = _stream->readByte();
|
|
|
|
if (c == '<')
|
|
keyOpening = currentPosition - 1;
|
|
else if (c == '>')
|
|
keyClosing = currentPosition;
|
|
}
|
|
|
|
_stream->seek(startPosition, SEEK_SET);
|
|
currentPosition = startPosition;
|
|
while (keyClosing == 0 && c && currentPosition++) {
|
|
c = _stream->readByte();
|
|
|
|
if (c == '>')
|
|
keyClosing = currentPosition;
|
|
}
|
|
|
|
currentPosition = (keyClosing - keyOpening);
|
|
_stream->seek(keyOpening, SEEK_SET);
|
|
|
|
while (currentPosition--)
|
|
errorMessage += (char)_stream->readByte();
|
|
}
|
|
|
|
errorMessage += "\n\nParser error: ";
|
|
errorMessage += errStr;
|
|
errorMessage += "\n\n";
|
|
|
|
g_system->logMessage(LogMessageType::kError, errorMessage.c_str());
|
|
|
|
return false;
|
|
}
|
|
|
|
bool XMLParser::parseXMLHeader(ParserNode *node) {
|
|
assert(node->header);
|
|
|
|
if (_activeKey.size() != 1)
|
|
return parserError("XML Header is expected in the global scope.");
|
|
|
|
if (!node->values.contains("version"))
|
|
return parserError("Missing XML version in XML header.");
|
|
|
|
if (node->values["version"] != "1.0")
|
|
return parserError("Unsupported XML version.");
|
|
|
|
return true;
|
|
}
|
|
|
|
bool XMLParser::parseActiveKey(bool closed) {
|
|
bool ignore = false;
|
|
assert(_activeKey.empty() == false);
|
|
|
|
ParserNode *key = _activeKey.top();
|
|
|
|
if (key->name == "xml" && key->header == true) {
|
|
assert(closed);
|
|
return parseXMLHeader(key) && closeKey();
|
|
}
|
|
|
|
XMLKeyLayout *layout = (_activeKey.size() == 1) ? _XMLkeys : getParentNode(key)->layout;
|
|
|
|
if (layout->children.contains(key->name)) {
|
|
key->layout = layout->children[key->name];
|
|
|
|
StringMap localMap = key->values;
|
|
int keyCount = localMap.size();
|
|
|
|
for (List<XMLKeyLayout::XMLKeyProperty>::const_iterator i = key->layout->properties.begin(); i != key->layout->properties.end(); ++i) {
|
|
if (i->required && !localMap.contains(i->name))
|
|
return parserError("Missing required property '" + i->name + "' inside key '" + key->name + "'");
|
|
else if (localMap.contains(i->name))
|
|
keyCount--;
|
|
}
|
|
|
|
if (keyCount > 0)
|
|
return parserError("Unhandled property inside key '" + key->name + "'.");
|
|
|
|
} else {
|
|
return parserError("Unexpected key in the active scope ('" + key->name + "').");
|
|
}
|
|
|
|
// check if any of the parents must be ignored.
|
|
// if a parent is ignored, all children are too.
|
|
for (int i = _activeKey.size() - 1; i >= 0; --i) {
|
|
if (_activeKey[i]->ignore)
|
|
ignore = true;
|
|
}
|
|
|
|
if (ignore == false && keyCallback(key) == false) {
|
|
// HACK: People may be stupid and overlook the fact that
|
|
// when keyCallback() fails, a parserError() must be set.
|
|
// We set it manually in that case.
|
|
if (_state != kParserError)
|
|
parserError("Unhandled exception when parsing '" + key->name + "' key.");
|
|
|
|
return false;
|
|
}
|
|
|
|
if (closed)
|
|
return closeKey();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool XMLParser::parseKeyValue(String keyName) {
|
|
assert(_activeKey.empty() == false);
|
|
|
|
if (_activeKey.top()->values.contains(keyName))
|
|
return false;
|
|
|
|
_token.clear();
|
|
char stringStart;
|
|
|
|
if (_char == '"' || _char == '\'') {
|
|
stringStart = _char;
|
|
_char = _stream->readByte();
|
|
|
|
while (_char && _char != stringStart) {
|
|
_token += _char;
|
|
_char = _stream->readByte();
|
|
}
|
|
|
|
if (_char == 0)
|
|
return false;
|
|
|
|
_char = _stream->readByte();
|
|
|
|
} else if (!parseToken()) {
|
|
return false;
|
|
}
|
|
|
|
_activeKey.top()->values[keyName] = _token;
|
|
return true;
|
|
}
|
|
|
|
bool XMLParser::parseIntegerKey(const char *key, int count, ...) {
|
|
bool result;
|
|
va_list args;
|
|
va_start(args, count);
|
|
result = vparseIntegerKey(key, count, args);
|
|
va_end(args);
|
|
return result;
|
|
}
|
|
|
|
bool XMLParser::parseIntegerKey(const String &key, int count, ...) {
|
|
bool result;
|
|
va_list args;
|
|
va_start(args, count);
|
|
result = vparseIntegerKey(key.c_str(), count, args);
|
|
va_end(args);
|
|
return result;
|
|
}
|
|
|
|
bool XMLParser::vparseIntegerKey(const char *key, int count, va_list args) {
|
|
char *parseEnd;
|
|
int *num_ptr;
|
|
|
|
while (count--) {
|
|
while (isSpace(*key))
|
|
key++;
|
|
|
|
num_ptr = va_arg(args, int*);
|
|
*num_ptr = strtol(key, &parseEnd, 10);
|
|
|
|
key = parseEnd;
|
|
|
|
while (isSpace(*key))
|
|
key++;
|
|
|
|
if (count && *key++ != ',')
|
|
return false;
|
|
}
|
|
|
|
return (*key == 0);
|
|
}
|
|
|
|
bool XMLParser::closeKey() {
|
|
bool ignore = false;
|
|
bool result = true;
|
|
|
|
for (int i = _activeKey.size() - 1; i >= 0; --i) {
|
|
if (_activeKey[i]->ignore)
|
|
ignore = true;
|
|
}
|
|
|
|
if (ignore == false)
|
|
result = closedKeyCallback(_activeKey.top());
|
|
|
|
freeNode(_activeKey.pop());
|
|
|
|
return result;
|
|
}
|
|
|
|
bool XMLParser::parse() {
|
|
if (_stream == nullptr)
|
|
return false;
|
|
|
|
// Make sure we are at the start of the stream.
|
|
_stream->seek(0, SEEK_SET);
|
|
|
|
if (_XMLkeys == nullptr)
|
|
buildLayout();
|
|
|
|
while (!_activeKey.empty())
|
|
freeNode(_activeKey.pop());
|
|
|
|
cleanup();
|
|
|
|
bool activeClosure = false;
|
|
bool activeHeader = false;
|
|
bool selfClosure;
|
|
|
|
_state = kParserNeedHeader;
|
|
_activeKey.clear();
|
|
|
|
_char = _stream->readByte();
|
|
|
|
while (_char && _state != kParserError) {
|
|
if (skipSpaces())
|
|
continue;
|
|
|
|
if (skipComments())
|
|
continue;
|
|
|
|
switch (_state) {
|
|
case kParserNeedHeader:
|
|
case kParserNeedKey:
|
|
if (_char != '<') {
|
|
parserError("Parser expecting key start.");
|
|
break;
|
|
}
|
|
|
|
if ((_char = _stream->readByte()) == 0) {
|
|
parserError("Unexpected end of file.");
|
|
break;
|
|
}
|
|
|
|
if (_state == kParserNeedHeader) {
|
|
if (_char != '?') {
|
|
parserError("Expecting XML header.");
|
|
break;
|
|
}
|
|
|
|
_char = _stream->readByte();
|
|
activeHeader = true;
|
|
} else if (_char == '/') {
|
|
_char = _stream->readByte();
|
|
activeClosure = true;
|
|
} else if (_char == '?') {
|
|
parserError("Unexpected header. There may only be one XML header per file.");
|
|
break;
|
|
}
|
|
|
|
_state = kParserNeedKeyName;
|
|
break;
|
|
|
|
case kParserNeedKeyName:
|
|
if (!parseToken()) {
|
|
parserError("Invalid key name.");
|
|
break;
|
|
}
|
|
|
|
if (activeClosure) {
|
|
if (_activeKey.empty() || _token != _activeKey.top()->name) {
|
|
parserError("Unexpected closure.");
|
|
break;
|
|
}
|
|
} else {
|
|
ParserNode *node = allocNode(); // new ParserNode;
|
|
node->name = _token;
|
|
node->ignore = false;
|
|
node->header = activeHeader;
|
|
node->depth = _activeKey.size();
|
|
node->layout = nullptr;
|
|
_activeKey.push(node);
|
|
}
|
|
|
|
_state = kParserNeedPropertyName;
|
|
break;
|
|
|
|
case kParserNeedPropertyName:
|
|
if (activeClosure) {
|
|
if (!closeKey()) {
|
|
parserError("Missing data when closing key '" + _activeKey.top()->name + "'.");
|
|
break;
|
|
}
|
|
|
|
activeClosure = false;
|
|
|
|
if (_char != '>')
|
|
parserError("Invalid syntax in key closure.");
|
|
else
|
|
_state = kParserNeedKey;
|
|
|
|
_char = _stream->readByte();
|
|
break;
|
|
}
|
|
|
|
selfClosure = false;
|
|
|
|
if (_char == '/' || (_char == '?' && activeHeader)) {
|
|
selfClosure = true;
|
|
_char = _stream->readByte();
|
|
}
|
|
|
|
if (_char == '>') {
|
|
if (activeHeader && !selfClosure) {
|
|
parserError("XML Header must be self-closed.");
|
|
} else if (parseActiveKey(selfClosure)) {
|
|
_char = _stream->readByte();
|
|
_state = kParserNeedKey;
|
|
}
|
|
|
|
activeHeader = false;
|
|
break;
|
|
}
|
|
|
|
if (selfClosure)
|
|
parserError("Expecting key closure after '/' symbol.");
|
|
else if (!parseToken())
|
|
parserError("Error when parsing key value.");
|
|
else
|
|
_state = kParserNeedPropertyOperator;
|
|
|
|
break;
|
|
|
|
case kParserNeedPropertyOperator:
|
|
if (_char != '=')
|
|
parserError("Syntax error after key name.");
|
|
else
|
|
_state = kParserNeedPropertyValue;
|
|
|
|
_char = _stream->readByte();
|
|
break;
|
|
|
|
case kParserNeedPropertyValue:
|
|
if (!parseKeyValue(_token))
|
|
parserError("Invalid key value.");
|
|
else
|
|
_state = kParserNeedPropertyName;
|
|
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (_state == kParserError)
|
|
return false;
|
|
|
|
if (_state != kParserNeedKey || !_activeKey.empty())
|
|
return parserError("Unexpected end of file.");
|
|
|
|
return true;
|
|
}
|
|
|
|
bool XMLParser::skipSpaces() {
|
|
if (!isSpace(_char))
|
|
return false;
|
|
|
|
while (_char && isSpace(_char))
|
|
_char = _stream->readByte();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool XMLParser::skipComments() {
|
|
if (_char == '<') {
|
|
_char = _stream->readByte();
|
|
|
|
if (_char != '!') {
|
|
_stream->seek(-1, SEEK_CUR);
|
|
_char = '<';
|
|
return false;
|
|
}
|
|
|
|
if (_stream->readByte() != '-' || _stream->readByte() != '-')
|
|
return parserError("Malformed comment syntax.");
|
|
|
|
_char = _stream->readByte();
|
|
|
|
while (_char) {
|
|
if (_char == '-') {
|
|
if (_stream->readByte() == '-') {
|
|
|
|
if (_stream->readByte() != '>')
|
|
return parserError("Malformed comment (double-hyphen inside comment body).");
|
|
|
|
_char = _stream->readByte();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
_char = _stream->readByte();
|
|
}
|
|
|
|
return parserError("Comment has no closure.");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool XMLParser::parseToken() {
|
|
_token.clear();
|
|
|
|
while (isValidNameChar(_char)) {
|
|
_token += _char;
|
|
_char = _stream->readByte();
|
|
}
|
|
|
|
return isSpace(_char) != 0 || _char == '>' || _char == '=' || _char == '/';
|
|
}
|
|
|
|
} // End of namespace Common
|