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

488 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/>.
*
*
* This file is dual-licensed.
* In addition to the GPLv3 license mentioned above, this code is also
* licensed under LGPL 2.1. See LICENSES/COPYING.LGPL file for the
* full text of the license.
*
*/
#include "gob/dbase.h"
#include "common/tokenizer.h"
namespace Gob {
dbaseMultipeIndex::dbaseMultipeIndex() {
clear();
}
void dbaseMultipeIndex::clear() {
memset(&_creationDate, 0, sizeof(_creationDate));
memset(&_lastUpdate, 0, sizeof(_lastUpdate));
_version = 0;
}
// Supported key definition syntax:
// key_definition ::= field_definition { "+" field_definition }
// field_definition ::= field_name | STR(field_name, length, 0)
Common::Array<dbaseMultipeIndex::FieldReference> dbaseMultipeIndex::parseKeyDefinition(const Common::String &keyDefinition) {
Common::Array<FieldReference> fieldReferences;
Common::StringTokenizer tokenizer(keyDefinition, "+");
while (!tokenizer.empty()) {
Common::String token = tokenizer.nextToken();
if (token.hasPrefix("STR(")) {
// STR(field_name, length, 0) expression
size_t firstCommaPos = token.find(',');
size_t secondCommaPos = token.find(',', firstCommaPos + 1);
Common::String fieldName = token.substr(4, firstCommaPos - 4);
size_t length = 0;
if (secondCommaPos != token.npos)
length = atoi(token.substr(firstCommaPos + 1, secondCommaPos - firstCommaPos - 1).c_str());
fieldReferences.push_back({fieldName, length});
} else {
// Field name only
fieldReferences.push_back({token, 0});
}
}
return fieldReferences;
}
const Common::Array<dbaseMultipeIndex::FieldReference>* dbaseMultipeIndex::getTagKeyDefinition(Common::String tagName) const {
if (!_tagKeyDefinitions.contains(tagName))
return nullptr;
else
return &_tagKeyDefinitions[tagName];
}
bool dbaseMultipeIndex::load(Common::SeekableReadStream &stream) {
_version = stream.readByte();
_creationDate.tm_year = stream.readByte();
_creationDate.tm_mon = stream.readByte() - 1;
_creationDate.tm_mday = stream.readByte();
uint32 pos = stream.pos();
_dataFilename = stream.readString('\0', 16);
stream.seek(pos + 16);
stream.skip(2); // Block size
stream.skip(2); // Page size
stream.skip(1); // Production flag
stream.skip(1); // Max number of tags
stream.skip(1); // Tag length
stream.skip(1); // Reserved
_nbrOfTagsInUse = stream.readUint16LE(); // Number of tags in use
stream.skip(2); // Reserved
stream.skip(4); // Number of pages in tag file
stream.skip(4); // Pointer to first free page
stream.skip(4); // Number of available blocks
_lastUpdate.tm_year = stream.readByte();
_lastUpdate.tm_mon = stream.readByte() - 1;
_lastUpdate.tm_mday = stream.readByte();
stream.seek(544); // Go past the header (size 32 + 512)
for (int i = 0; i < _nbrOfTagsInUse; ++i) {
uint32 tagHeaderPage = stream.readUint32LE();
Common::String tagName = stream.readString('\0', 11);
stream.skip(1); // key format
stream.skip(1); // forward tag thread inf
stream.skip(1); // forward tag thread sup
stream.skip(1); // backward tag thread
stream.skip(1); // reserved
stream.skip(1); // key type
stream.skip(11); // reserved
int64 tagEntryEnd = stream.pos();
stream.seek(tagHeaderPage * INDEX_PAGE_SIZE);
stream.skip(4); // tag root page
stream.skip(4); // file size in pages
stream.skip(1); // key format
stream.skip(1); // key type
stream.skip(2); // reserved
stream.skip(2); // index key length
stream.skip(2); // max nbr of keys per page
stream.skip(2); // secondary key type
stream.skip(2); // index key item length
stream.skip(3); // reserved
stream.skip(1); // unique flag
Common::String keyExpression = stream.readString('\0', 488);
_tagKeyDefinitions[tagName] = parseKeyDefinition(keyExpression);
// For now, we do not need to read the B-tree structure itself.
// The key definition is enough for our use cases.
stream.seek(tagEntryEnd);
}
return true;
}
dBase::dBase() : _recordData(nullptr) {
clear();
}
dBase::~dBase() {
clear();
}
bool dBase::load(Common::SeekableReadStream &stream) {
clear();
uint32 startPos = stream.pos();
_version = stream.readByte();
if (_version == 0x03 || _version == 0x83) {
_versionMajor = 3;
} else if (_version == 0x04 || _version == 0x7B || _version == 0x8B) {
_versionMajor = 4;
} else {
warning("dBase::load() called on unsupported dBase version %d", _version);
return false;
}
_hasMemo = (_version & 0x80) != 0;
_lastUpdate.tm_year = stream.readByte();
_lastUpdate.tm_mon = stream.readByte() - 1;
_lastUpdate.tm_mday = stream.readByte();
_lastUpdate.tm_hour = 0;
_lastUpdate.tm_min = 0;
_lastUpdate.tm_sec = 0;
uint32 recordCount = stream.readUint32LE();
uint32 headerSize = stream.readUint16LE();
uint32 recordSize = stream.readUint16LE();
stream.skip(20); // Reserved
// Read all field descriptions, 0x0D is the end marker
uint32 fieldsLength = 0;
while (!stream.eos() && !stream.err() && (stream.readByte() != 0x0D)) {
Field field;
stream.seek(-1, SEEK_CUR);
field.name = readString(stream, 11);
field.type = (Type) stream.readByte();
stream.skip(4); // Field data address
field.size = stream.readByte();
field.decimals = stream.readByte();
fieldsLength += field.size;
stream.skip(14); // Reserved and/or useless for us
_fields.push_back(field);
}
if (stream.eos() || stream.err())
return false;
if ((stream.pos() - startPos) != headerSize)
// Corrupted file / unknown format
return false;
if (recordSize != (fieldsLength + 1))
// Corrupted file / unknown format
return false;
_recordData = new byte[recordSize * recordCount];
if (stream.read(_recordData, recordSize * recordCount) != (recordSize * recordCount))
return false;
if (stream.readByte() != 0x1A)
// Missing end marker
return false;
uint32 fieldCount = _fields.size();
// Create the records array
_records.resize(recordCount);
for (uint32 i = 0; i < recordCount; i++) {
Record &record = _records[i];
const byte *data = _recordData + i * recordSize;
char status = *data++;
if ((status != ' ') && (status != '*'))
// Corrupted file / unknown format
return false;
record.deleted = status == '*';
record.fields.resize(fieldCount);
for (uint32 j = 0; j < fieldCount; j++) {
record.fields[j] = data;
data += _fields[j].size;
}
}
return true;
}
bool dBase::loadMemo(Common::SeekableReadStream &stream) {
uint32 nextBlock = stream.readUint32LE();
if (nextBlock < 2)
return true; // No data blocks
uint32 nbrOfDataBlocks = nextBlock - 2;
_memoData.clear();
_memoData.resize(nbrOfDataBlocks * MEMO_BLOCK_SIZE);
for (uint32 i = 1; i < nextBlock; i++) {
stream.seek(i * MEMO_BLOCK_SIZE, SEEK_SET);
uint32 type = stream.readUint32LE();
if (type != 0x8FFFF) {
warning("dBase::loadMemo() found unexpected memo record type %08X", type);
}
int32 memoSize = stream.readSint32LE();
if (memoSize < 8) // Header size (8) is included in memoSize
continue; // Empty memo
_memoData[i - 1] = readString(stream, memoSize - 8);
}
return true;
}
bool dBase::loadMultipleIndex(Common::SeekableReadStream &stream) {
if (_multipleIndex.load(stream)) {
_hasMultipleIndex = true;
return true;
} else
return false;
}
void dBase::clear() {
memset(&_lastUpdate, 0, sizeof(_lastUpdate));
_version = 0;
_hasMemo = false;
_fields.clear();
_records.clear();
delete[] _recordData;
_recordData = nullptr;
}
byte dBase::getVersion() const {
return _version;
}
bool dBase::hasMemo() const {
return _hasMemo;
}
TimeDate dBase::getLastUpdate() const {
return _lastUpdate;
}
const Common::Array<dBase::Field> &dBase::getFields() const {
return _fields;
}
const Common::Array<dBase::Record> &dBase::getRecords() const {
return _records;
}
Common::String dBase::getString(const Record &record, int field) const {
Type type = _fields[field].type;
switch (type) {
case kTypeString: {
uint32 fieldLength = stringLength(record.fields[field], _fields[field].size);
return Common::String((const char *) record.fields[field], fieldLength);
}
case kTypeNumber: {
Common::String str = Common::String((const char *) record.fields[field], _fields[field].size);
str.trim();
return str;
}
case kTypeMemo: {
Common::String blockNbrStr = Common::String((const char *) record.fields[field], _fields[field].size);
int blockNbr = atoi(blockNbrStr.c_str());
if ((blockNbr < 1) || ((size_t) blockNbr > _memoData.size())) {
warning("dBase::getString() called on invalid memo block %d", blockNbr);
return "";
}
return _memoData[blockNbr - 1];
}
default:
// Unsupported type
warning("dBase::getString() called on unsupported field type %d", type);
}
return "";
}
void dBase::setQuery(const Common::String &query) {
_currentFieldFilter.clear();
if (!_hasMultipleIndex) {
warning("dBase::setQuery() called on a database without multiple index");
return;
}
const Common::Array<dbaseMultipeIndex::FieldReference>* keyDefinition = _multipleIndex.getTagKeyDefinition(_currentIndexTag);
if (!keyDefinition) {
warning("dBase::setQuery(): key definition not found for tag '%s'", _currentIndexTag.c_str());
return;
}
// Parse the query. Field separator is ';', catch-all is '?'
Common::StringTokenizer tokenizer(query, ";");
size_t fieldIndex = 0;
while (!tokenizer.empty()) {
Common::String token = tokenizer.nextToken();
if (token != "?") {
if (fieldIndex >= keyDefinition->size()) {
warning("dBase::setQuery(): too many fields in query");
return;
}
const dbaseMultipeIndex::FieldReference &fieldReference = (*keyDefinition)[fieldIndex];
const Common::String &fieldName = fieldReference.getFieldName();
for (size_t i = 0; i < _fields.size(); ++i) {
if (_fields[i].name == fieldName) {
_currentFieldFilter.push_back({i, fieldReference.getMaxLength(), token});
break;
}
if (i == _fields.size() - 1) {
warning("dBase::setQuery(): field '%s' not found", fieldName.c_str());
return;
}
}
}
++fieldIndex;
}
}
void dBase::setCurrentIndex(const Common::String &tagName) {
_currentIndexTag = tagName;
_currentRecordIndex = -1;
_currentFieldFilter.clear();
}
void dBase::findNextMatchingRecord() {
++_currentRecordIndex;
if (_currentFieldFilter.empty()) {
_currentRecordIndex = _records.size();
return;
}
for (; _currentRecordIndex < (int)_records.size(); ++_currentRecordIndex) {
const Record &record = _records[_currentRecordIndex];
bool match = true;
for (const FieldPattern &pattern : _currentFieldFilter) {
if (pattern.fieldIndex >= _fields.size()) {
match = false;
break;
}
Common::String fieldValue = getString(record, pattern.fieldIndex);
if (pattern.maxLength > 0)
fieldValue = fieldValue.substr(0, pattern.maxLength);
if (fieldValue != pattern.pattern) {
match = false;
break;
}
}
if (match)
return;
}
}
void dBase::findFirstMatchingRecord() {
_currentRecordIndex = -1;
findNextMatchingRecord();
}
bool dBase::hasMatchingRecord() {
return _currentRecordIndex >= 0 && _currentRecordIndex < (int) _records.size();
}
Common::String dBase::getFieldOfMatchingRecord(Common::String fieldName) {
if (!hasMatchingRecord())
return "";
const Record &record = _records[_currentRecordIndex];
size_t fieldIndex = 0;
for (const Field &field : _fields) {
if (field.name == fieldName) {
return getString(record, fieldIndex);
}
++fieldIndex;
}
return "";
}
// String fields are padded with spaces. This finds the real length.
inline uint32 dBase::stringLength(const byte *data, uint32 max) {
while (max-- > 0)
if ((data[max] != 0x20) && (data[max] != 0x00))
return max + 1;
return 0;
}
// Read a constant-length string out of a stream.
inline Common::String dBase::readString(Common::SeekableReadStream &stream, int n) {
Common::String str;
char c;
while (n-- > 0) {
if ((c = stream.readByte()) == '\0')
break;
str += c;
}
if (n > 0)
stream.skip(n);
return str;
}
} // End of namespace Gob