/* 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 . * */ #include "common/file.h" #include "common/util.h" #include "common/fs.h" #include "common/debug.h" #include "common/textconsole.h" #include "common/ini-file.h" #include "agi/wagparser.h" namespace Agi { WagProperty::WagProperty() { setDefaults(); } WagProperty::~WagProperty() { deleteData(); } WagProperty::WagProperty(const WagProperty &other) { deepCopy(other); } WagProperty &WagProperty::operator=(const WagProperty &other) { if (&other != this) deepCopy(other); // Don't do self-assignment return *this; } void WagProperty::deepCopy(const WagProperty &other) { _readOk = other._readOk; _propCode = other._propCode; _propType = other._propType; _propNum = other._propNum; _propSize = other._propSize; if (other._propData != nullptr) { _propData = new char[other._propSize + 1UL]; // Allocate space for property's data plus trailing zero memcpy(_propData, other._propData, other._propSize + 1UL); // Copy the whole thing } } bool WagProperty::read(Common::SeekableReadStream &stream) { // First read the property's header _propCode = (enum WagPropertyCode)stream.readByte(); _propType = (enum WagPropertyType)stream.readByte(); _propNum = stream.readByte(); _propSize = stream.readUint16LE(); if (stream.eos() || stream.err()) { // Check that we got the whole header _readOk = false; return _readOk; } // Then read the property's data _propData = new char[_propSize + 1UL]; // Allocate space for property's data plus trailing zero uint32 readBytes = stream.read(_propData, _propSize); // Read the data in _propData[_propSize] = 0; // Set the trailing zero for easy C-style string access _readOk = (readBytes == _propSize); // Check that we got the whole data return _readOk; } void WagProperty::clear() { deleteData(); setDefaults(); } void WagProperty::setDefaults() { _readOk = false; _propCode = PC_UNDEFINED; _propType = PT_UNDEFINED; _propNum = 0; _propSize = 0; _propData = nullptr; } void WagProperty::deleteData() { if (_propData) delete[] _propData; _propData = nullptr; } WagFileParser::WagFileParser() : _parsedOk(false) { } WagFileParser::~WagFileParser() { } bool WagFileParser::checkAgiVersionProperty(const WagProperty &version) const { if (version.getCode() == WagProperty::PC_INTVERSION && // Must be AGI interpreter version property version.getSize() >= 3 && // Need at least three characters for a version number like "X.Y" Common::isDigit(version.getData()[0]) && // And the first character must be a digit (version.getData()[1] == ',' || version.getData()[1] == '.')) { // And the second a comma or a period for (int i = 2; i < version.getSize(); i++) // And the rest must all be digits if (!Common::isDigit(version.getData()[i])) return false; // Bail out if found a non-digit after the decimal point return true; } else // Didn't pass the preliminary test so fails return false; } uint16 WagFileParser::convertToAgiVersionNumber(const WagProperty &version) { // Examples of the conversion: "2.44" -> 0x2440, "2.917" -> 0x2917, "3.002086" -> 0x3086. if (checkAgiVersionProperty(version)) { // Check that the string is a valid AGI interpreter version string // Convert first ascii digit to an integer and put it in the fourth nibble (Bits 12...15) of the version number // and at the same time set all other nibbles to zero. uint16 agiVerNum = ((uint16)(version.getData()[0] - '0')) << (3 * 4); // Convert at most three least significant digits of the version number's minor part // (i.e. the part after the decimal point) and put them in order to the third, second // and the first nibble of the version number. Just to clarify version.getSize() - 2 // is the number of digits after the decimal point. int32 digitCount = MIN(3, ((int32) version.getSize()) - 2); // How many digits left to convert for (int i = 0; i < digitCount; i++) agiVerNum |= ((uint16)(version.getData()[version.getSize() - digitCount + i] - '0')) << ((2 - i) * 4); debug(3, "WagFileParser: Converted AGI version from string %s to number 0x%x", version.getData(), agiVerNum); return agiVerNum; } else // Not a valid AGI interpreter version string return 0; // Can't convert, so failure } bool WagFileParser::checkWagVersion(Common::SeekableReadStream &stream) { if (stream.size() >= WINAGI_VERSION_LENGTH) { // Stream has space to contain the WinAGI version string // Read the last WINAGI_VERSION_LENGTH bytes of the stream and make a string out of it char str[WINAGI_VERSION_LENGTH + 1]; // Allocate space for the trailing zero also uint32 oldStreamPos = stream.pos(); // Save the old stream position stream.seek(stream.size() - WINAGI_VERSION_LENGTH); uint32 readBytes = stream.read(str, WINAGI_VERSION_LENGTH); stream.seek(oldStreamPos); // Seek back to the old stream position str[readBytes] = 0; // Set the trailing zero to finish the C-style string if (readBytes != WINAGI_VERSION_LENGTH) { // Check that we got the whole version string debug(3, "WagFileParser::checkWagVersion: Error reading WAG file version from stream"); return false; } debug(3, "WagFileParser::checkWagVersion: Read WinAGI version string (\"%s\")", str); // Check that the WinAGI version string is one of the two version strings // WinAGI 1.1.21 recognizes as acceptable in the end of a *.wag file. // Note that they are all of length 16 and are padded with spaces to be that long. return scumm_stricmp(str, "WINAGI v1.0 ") == 0 || scumm_stricmp(str, "1.0 BETA ") == 0; } else { // Stream is too small to contain the WinAGI version string debug(3, "WagFileParser::checkWagVersion: Stream is too small to contain a valid WAG file"); return false; } } void WagFileParser::addPropFromIni(Common::INIFile *iniWagFile, Common::String section, Common::String key, Agi::WagProperty::WagPropertyCode code) { WagProperty property; property.setPropCode(code); Common::String value; if (iniWagFile->getKey(key, section, value)) { property.setPropDataSize(value); _propList.push_back(property); } } bool WagFileParser::parse(const Common::FSNode &node) { WagProperty property; // Temporary property used for reading Common::SeekableReadStream *stream = nullptr; // The file stream _parsedOk = false; // We haven't parsed the file yet stream = node.createReadStream(); // Open the file if (stream) { // Check that opening the file was successful if (checkWagVersion(*stream)) { // Check that WinAGI version string is valid // It seems we've got a valid *.wag file so let's parse its properties from the start. stream->seek(0); // Rewind the stream if (!_propList.empty()) _propList.clear(); // Clear out old properties (If any) do { // Parse the properties if (property.read(*stream)) { // Read the property and check it was read ok _propList.push_back(property); // Add read property to properties list debug(4, "WagFileParser::parse: Read property with code %d, type %d, number %d, size %d, data \"%s\"", property.getCode(), property.getType(), property.getNumber(), property.getSize(), property.getData()); } else // Reading failed, let's bail out break; } while (!endOfProperties(*stream)); // Loop until the end of properties // File was parsed successfully only if we got to the end of properties // and all the properties were read successfully (Also the last). _parsedOk = endOfProperties(*stream) && property.readOk(); if (!_parsedOk) // Error parsing stream warning("Error parsing WAG file (%s). WAG file ignored", node.getPath().c_str()); } else { // Invalid WinAGI version string or it couldn't be read // Let's try to read WAG file as newer INI format Common::INIFile *iniWagFile = new Common::INIFile(); _parsedOk = iniWagFile->loadFromStream(*stream); if (_parsedOk) { addPropFromIni(iniWagFile, "General", "Interpreter", WagProperty::PC_INTVERSION); addPropFromIni(iniWagFile, "General", "GameID", WagProperty::PC_GAMEID); addPropFromIni(iniWagFile, "General", "Description", WagProperty::PC_GAMEDESC); addPropFromIni(iniWagFile, "General", "GameVersion", WagProperty::PC_GAMEVERSION); addPropFromIni(iniWagFile, "General", "LastEdit", WagProperty::PC_GAMELAST); } else warning("Invalid WAG file (%s) version or error reading it. WAG file ignored", node.getPath().c_str()); } } else // Couldn't open file warning("Couldn't open WAG file (%s). WAG file ignored", node.getPath().c_str()); delete stream; return _parsedOk; } const WagProperty *WagFileParser::getProperty(const WagProperty::WagPropertyCode code) const { for (PropertyList::const_iterator iter = _propList.begin(); iter != _propList.end(); ++iter) if (iter->getCode() == code) return iter; return nullptr; } bool WagFileParser::endOfProperties(const Common::SeekableReadStream &stream) const { return stream.pos() >= (stream.size() - WINAGI_VERSION_LENGTH); } void WagProperty::setPropCode(WagPropertyCode propCode) { _propCode = propCode; } void WagProperty::setPropDataSize(Common::String str) { _propData = scumm_strdup(str.c_str()); _propSize = str.size(); } } // End of namespace Agi