scummvm/engines/bagel/boflib/dat_file.cpp
2024-07-27 12:51:03 +03:00

818 lines
19 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 "bagel/boflib/dat_file.h"
#include "bagel/boflib/crc.h"
#include "bagel/boflib/debug.h"
#include "bagel/boflib/log.h"
#include "bagel/boflib/misc.h"
#include "bagel/boflib/stdinc.h"
#include "bagel/boflib/file_functions.h"
namespace Bagel {
// Local prototypes
static uint32 CreateHashCode(const byte *);
void HeaderRec::synchronize(Common::Serializer &s) {
s.syncAsSint32LE(_lOffset);
s.syncAsSint32LE(_lLength);
s.syncAsUint32LE(_lCrc);
s.syncAsUint32LE(_lKey);
}
void HeadInfo::synchronize(Common::Serializer &s) {
s.syncAsSint32LE(_lNumRecs);
s.syncAsSint32LE(_lAddress);
s.syncAsUint32LE(_lFlags);
s.syncAsUint32LE(_lFootCrc);
}
CBofDataFile::CBofDataFile() {
_szFileName[0] = '\0';
_szPassWord[0] = '\0';
_lHeaderLength = 0;
_lNumRecs = 0;
_pHeader = nullptr;
_bHeaderDirty = false;
}
ErrorCode CBofDataFile::setFile(const char *pszFileName, uint32 lFlags) {
assert(isValidObject(this));
// Validate input
assert(pszFileName != nullptr);
assert(strlen(pszFileName) < MAX_FNAME);
// Release any previous data-file
releaseFile();
// All data files are binary, so force it
lFlags |= CBF_BINARY;
// Remember the flags
_lFlags = lFlags;
if (fileGetFullPath(_szFileName, pszFileName) != nullptr) {
if (open() == ERR_NONE) {
// Read header block
readHeader();
// Close data file if we are not keeping it open
if (!(_lFlags & CDF_KEEPOPEN)) {
close();
}
} else
reportError(ERR_FOPEN, "Could not open file %s", _szFileName);
} else {
reportError(ERR_FFIND, "Could not build full path to %s", pszFileName);
}
return _errCode;
}
CBofDataFile::~CBofDataFile() {
assert(isValidObject(this));
releaseFile();
}
ErrorCode CBofDataFile::releaseFile() {
assert(isValidObject(this));
// If header was modified
if (_bHeaderDirty) {
// Write header to disk
writeHeader();
}
close();
// Free header buffer
delete[] _pHeader;
_pHeader = nullptr;
return _errCode;
}
ErrorCode CBofDataFile::create() {
assert(isValidObject(this));
HeadInfo stHeaderInfo;
// Only continue if there is no current error
if (_errCode == ERR_NONE) {
if (_stream != nullptr) {
close();
}
// Re-initialize
delete[] _pHeader;
_pHeader = nullptr;
_stream = nullptr;
_lHeaderLength = 0;
_bHeaderDirty = false;
stHeaderInfo._lNumRecs = _lNumRecs = 0;
stHeaderInfo._lAddress = HeadInfo::size();
// Create the file
if (CBofFile::create(_szFileName, _lFlags) == ERR_NONE) {
// Write empty header info
if (write(stHeaderInfo) != ERR_NONE) {
_errCode = ERR_FWRITE;
}
seek(0);
} else {
_errCode = ERR_FOPEN;
}
}
return _errCode;
}
ErrorCode CBofDataFile::open() {
assert(isValidObject(this));
// Only continue if there is no current error
if (_errCode == ERR_NONE && _stream == nullptr) {
if (!(_lFlags & CDF_READONLY)) {
if (_lFlags & CDF_SAVEFILE) {
if (_lFlags & CDF_CREATE)
create();
} else if (!fileExists(_szFileName))
create();
}
if (_stream == nullptr) {
// Open data file
CBofFile::open(_szFileName, _lFlags);
}
}
return _errCode;
}
ErrorCode CBofDataFile::close() {
assert(isValidObject(this));
if (_stream != nullptr) {
if (_bHeaderDirty) {
writeHeader();
}
CBofFile::close();
}
return _errCode;
}
ErrorCode CBofDataFile::readHeader() {
assert(isValidObject(this));
// Only continue if there is no current error
if (_errCode == ERR_NONE) {
if (_stream == nullptr) {
open();
}
if (!errorOccurred()) {
// Determine number of records in file
HeadInfo stHeaderInfo;
if (read(stHeaderInfo) == ERR_NONE) {
_lNumRecs = stHeaderInfo._lNumRecs;
_lHeaderStart = stHeaderInfo._lAddress;
// Length of header is number of records * header-record size
_lHeaderLength = _lNumRecs * HeaderRec::size();
Common::SeekableReadStream *rs = dynamic_cast<Common::SeekableReadStream *>(_stream);
assert(rs);
int32 lfileLength = rs->size();
// Make sure header contains valid info
if ((_lHeaderStart >= HeadInfo::size()) &&
(_lHeaderStart <= lfileLength) && (_lHeaderLength >= 0) &&
(_lHeaderLength < lfileLength)) {
// Force Encrypted, and Compress if existing file has them
_lFlags |= stHeaderInfo._lFlags & CDF_ENCRYPT;
_lFlags |= stHeaderInfo._lFlags & CDF_COMPRESSED;
if (_lHeaderLength != 0) {
// Allocate buffer to hold header
_pHeader = new HeaderRec[(int)_lNumRecs];
// Seek to start of header
seek(_lHeaderStart);
// Read header
ErrorCode errorCode = ERR_NONE;
for (int i = 0; i < _lNumRecs && errorCode == ERR_NONE; ++i) {
errorCode = read(_pHeader[i]);
}
if (errorCode == ERR_NONE) {
uint32 lCrc = calculateCRC(&_pHeader->_lOffset, 4 * _lNumRecs);
if (lCrc != stHeaderInfo._lFootCrc) {
logError(buildString("Error: '%s' has invalid footer", _szFileName));
_errCode = ERR_CRC;
}
} else {
logError(buildString("Error: Could not read footer in file '%s'", _szFileName));
_errCode = ERR_FREAD;
}
}
} else {
logError(buildString("Error: '%s' has invalid header", _szFileName));
_errCode = ERR_FTYPE;
}
} else {
logError(buildString("Error: Could not read header in file '%s'", _szFileName));
_errCode = ERR_FREAD;
}
}
}
return _errCode;
}
ErrorCode CBofDataFile::writeHeader() {
assert(isValidObject(this));
// Only continue if there is no current error
if (_errCode == ERR_NONE) {
// Open the data file if it's not already open
if (_stream == nullptr) {
open();
}
if (_errCode == ERR_NONE) {
// Header starts at the end of the last record
HeaderRec *pRec = &_pHeader[_lNumRecs - 1];
_lHeaderStart = pRec->_lOffset + pRec->_lLength;
HeadInfo stHeaderInfo;
stHeaderInfo._lNumRecs = _lNumRecs;
stHeaderInfo._lAddress = _lHeaderStart;
stHeaderInfo._lFlags = _lFlags;
stHeaderInfo._lFootCrc = calculateCRC(&_pHeader->_lOffset, 4 * _lNumRecs);
// Seek to front of file to write header info
seekToBeginning();
if (write(stHeaderInfo) == ERR_NONE) {
// Seek to start of where header is to be written
seek(_lHeaderStart);
// Write header to data file
ErrorCode errorCode = ERR_NONE;
for (int i = 0; i < _lNumRecs && errorCode == ERR_NONE; ++i) {
errorCode = write(_pHeader[i]);
}
if (errorCode == ERR_NONE) {
// Header is now clean
_bHeaderDirty = false;
} else {
logError(buildString("Error writing footer to file '%s'", _szFileName));
_errCode = ERR_FWRITE;
}
} else {
logError(buildString("Error writing header to file '%s'", _szFileName));
_errCode = ERR_FWRITE;
}
}
}
return _errCode;
}
ErrorCode CBofDataFile::readRecord(int32 lRecNum, void *pBuf) {
assert(isValidObject(this));
// Only continue if there is no current error
if (_errCode == ERR_NONE) {
// Can't write to nullptr pointers
assert(pBuf != nullptr);
// Validate record number
assert(lRecNum >= 0 && lRecNum < _lNumRecs);
// Make sure we have a valid header
assert(_pHeader != nullptr);
// Get info about address of where record starts
// and how large the record is.
HeaderRec *pRecInfo = &_pHeader[(int)lRecNum];
// Open the data file if it's not already open
if (_stream == nullptr) {
open();
}
if (_errCode == ERR_NONE) {
// Seek to that point in the file
seek(pRecInfo->_lOffset);
// Read in the record
if (read(pBuf, pRecInfo->_lLength) == ERR_NONE) {
// If this file is encrypted, then decrypt it
if (_lFlags & CDF_ENCRYPT) {
decrypt(pBuf, (int)pRecInfo->_lLength, _szPassWord);
}
// Calculate and verify this record's CRC value
uint32 lCrc = calculateCRC(pBuf, (int)pRecInfo->_lLength);
if (lCrc != pRecInfo->_lCrc) {
_errCode = ERR_CRC;
}
} else {
logError(buildString("Error reading record %d in file '%s'", lRecNum, _szFileName));
_errCode = ERR_FREAD;
}
}
}
return _errCode;
}
ErrorCode CBofDataFile::readFromFile(int32 lRecNum, void *pBuf, int32 lBytes) {
assert(isValidObject(this));
// Only continue if there is no current error
if (_errCode == ERR_NONE) {
// Can't write to nullptr pointers
assert(pBuf != nullptr);
// Validate record number
assert(lRecNum >= 0 && lRecNum < _lNumRecs);
// Make sure we have a valid header
assert(_pHeader != nullptr);
// Get info about address of where record starts
// and how large the record is.
HeaderRec *pRecInfo = &_pHeader[(int)lRecNum];
// Open the data file if it's not already open
if (_stream == nullptr) {
open();
}
if (_errCode == ERR_NONE) {
// Seek to that point in the file
seek(pRecInfo->_lOffset);
// Read in the requested bytes...
if (read(pBuf, lBytes) == ERR_NONE) {
// If this file is encrypted, then decrypt it
if (_lFlags & CDF_ENCRYPT) {
decryptPartial(pBuf, (int32)pRecInfo->_lLength, (int32)lBytes, _szPassWord);
}
// Don't bother with a CRC as this chunk of input won't generate a proper
// CRC anyway.
} else {
logError(buildString("Error reading record %u in file '%s'", lRecNum, _szFileName));
_errCode = ERR_FREAD;
}
}
}
return _errCode;
}
ErrorCode CBofDataFile::writeRecord(int32 lRecNum, void *pBuf, int32 lSize, bool bUpdateHeader, uint32 lKey) {
assert(isValidObject(this));
// Only continue if there is no current error
if (_errCode == ERR_NONE) {
// Validate record number
assert(lRecNum >= 0 && lRecNum < _lNumRecs);
// Validate input buffer
assert(pBuf != nullptr);
// There must already be a valid header
assert(_pHeader != nullptr);
if (lSize == -1)
lSize = _pHeader[(int)lRecNum]._lLength;
int32 lPrevOffset = HeadInfo::size();
int32 lPrevLength = 0;
if (lRecNum != 0) {
lPrevOffset = _pHeader[(int)lRecNum - 1]._lOffset;
lPrevLength = _pHeader[(int)lRecNum - 1]._lLength;
}
HeaderRec *pRecInfo = &_pHeader[(int)lRecNum];
// Header needs to updated
_bHeaderDirty = true;
if (_stream == nullptr) {
open();
}
// This record starts at the end of the last record
pRecInfo->_lOffset = lPrevOffset + lPrevLength;
// Seek to where we want to write this record
seek(pRecInfo->_lOffset);
// Calculate new hash code based on this records key
pRecInfo->_lKey = lKey;
if (lKey == 0xFFFFFFFF) {
pRecInfo->_lKey = CreateHashCode((const byte *)pBuf);
}
// Calculate this record's CRC value
pRecInfo->_lCrc = calculateCRC(pBuf, lSize);
if (_lFlags & CDF_ENCRYPT) {
encrypt(pBuf, lSize, _szPassWord);
}
// If new record is larger then original
if (lSize > pRecInfo->_lLength) {
// How many bytes back do we have to write?
int32 lDiff = lSize - pRecInfo->_lLength;
//
// Move the rest of file back that many bytes
//
// Read the rest of the file in chunks (of 200k or less),
// and write each chunk back in it's new position.
//
int32 lBufLength = getLength() - (pRecInfo->_lOffset + pRecInfo->_lLength);
int32 lChunkSize = MIN(lBufLength, (int32)200000);
// Allocate a buffer big enough for one chunk
byte *pTmpBuf = (byte *)bofAlloc(lChunkSize);
// While there is data to move
while (lBufLength > 0) {
// Seek to beginning of the source for this chunk
setPosition(pRecInfo->_lOffset + pRecInfo->_lLength + lBufLength - lChunkSize);
// Read the chunk
read(pTmpBuf, lChunkSize);
// Seek to this chunks new position (offset by 'lDiff' bytes)
setPosition(pRecInfo->_lOffset + pRecInfo->_lLength + lBufLength - lChunkSize + lDiff);
// Write chunk to new position
write(pTmpBuf, lChunkSize);
// That much less to do next time through
lBufLength -= lChunkSize;
// Last chunk is lBufLength
lChunkSize = MIN(lBufLength, lChunkSize);
}
// Don't need that temp buffer anymore
bofFree(pTmpBuf);
// Tell the rest of the records that they moved
for (int i = lRecNum + 1; i < getNumberOfRecs(); i++) {
_pHeader[i]._lOffset += lDiff;
}
// Remember it's new length
pRecInfo->_lLength = lSize;
// Seek to where we want to write this record
seek(pRecInfo->_lOffset);
// Write this record
write(pBuf, lSize);
// If we are to update the header now
if (bUpdateHeader) {
writeHeader();
}
} else {
// Write this record
if (write(pBuf, lSize) == ERR_NONE) {
// If this record got smaller
if (pRecInfo->_lLength > lSize) {
// Remember it's length
pRecInfo->_lLength = lSize;
int bufferSize = getMaxRecSize();
if (bufferSize <= 0)
fatalError(ERR_FREAD, "Invalid size read in header data");
// Allocate a buffer that could hold the largest record
byte *pTmpBuf = (byte *)bofAlloc(bufferSize);
for (int i = (int)lRecNum + 1; i < (int)_lNumRecs - 1; i++) {
_errCode = readRecord(i, pTmpBuf);
if (_errCode != ERR_NONE)
break;
_errCode = writeRecord(i + 1, pTmpBuf);
if (_errCode != ERR_NONE)
break;
}
bofFree(pTmpBuf);
}
// If we are to update the header now
if (bUpdateHeader) {
writeHeader();
}
} else {
_errCode = ERR_FWRITE;
}
}
// If this record is encrypted the decrypt it
if (_lFlags & CDF_ENCRYPT) {
decrypt(pBuf, (int)pRecInfo->_lLength, _szPassWord);
}
}
return _errCode;
}
ErrorCode CBofDataFile::verifyRecord(int32 lRecNum) {
assert(isValidObject(this));
if (_errCode == ERR_NONE) {
// Validate record number
assert(lRecNum >= 0 && lRecNum < _lNumRecs);
// Allocate space to hold this record
void *pBuf = bofAlloc((int)getRecSize(lRecNum));
_errCode = readRecord(lRecNum, pBuf);
bofFree(pBuf);
}
return _errCode;
}
ErrorCode CBofDataFile::verifyAllRecords() {
assert(isValidObject(this));
if (_errCode == ERR_NONE) {
int32 n = getNumberOfRecs();
for (int32 i = 0; i < n; i++) {
_errCode = verifyRecord(i);
if (_errCode != ERR_NONE) {
break;
}
}
}
return _errCode;
}
ErrorCode CBofDataFile::addRecord(void *pBuf, int32 lLength, bool bUpdateHeader, uint32 lKey) {
assert(isValidObject(this));
// Only continue if there is no current error
if (_errCode == ERR_NONE) {
// Validate input
assert(pBuf != nullptr);
assert(lLength > 0);
if (lLength > 0) {
if (_stream == nullptr) {
open();
}
if (_errCode == ERR_NONE) {
_lNumRecs++;
HeaderRec *pTmpHeader = new HeaderRec[(int)_lNumRecs];
for (int i = 0; i < _lNumRecs; ++i) {
pTmpHeader[i]._lOffset = pTmpHeader[i]._lLength = 0;
pTmpHeader[i]._lCrc = pTmpHeader[i]._lKey = 0;
}
if (_pHeader != nullptr) {
memcpy(pTmpHeader, _pHeader, (size_t)(HeaderRec::size() * (_lNumRecs - 1)));
delete[] _pHeader;
}
_pHeader = pTmpHeader;
int32 lRecNum = _lNumRecs - 1;
HeaderRec *pCurRec = &_pHeader[lRecNum];
int32 lPrevLength = HeadInfo::size();
int32 lPrevOffset = 0;
if (lRecNum != 0) {
lPrevLength = _pHeader[lRecNum - 1]._lLength;
lPrevOffset = _pHeader[lRecNum - 1]._lOffset;
}
pCurRec->_lLength = lLength;
pCurRec->_lOffset = lPrevOffset + lPrevLength;
writeRecord(lRecNum, pBuf, lLength, bUpdateHeader, lKey);
}
}
}
return _errCode;
}
int32 CBofDataFile::findRecord(uint32 lKey) {
assert(isValidObject(this));
// Assume no match
int32 lRecNum = -1;
// Only continue if there is no current error
if (_errCode == ERR_NONE) {
// Scan the header for the key matching the hash code
for (int32 i = 0; i < _lNumRecs; i++) {
// Header records must be valid
assert(_pHeader != nullptr);
if (_pHeader[i]._lKey == lKey) {
lRecNum = i;
break;
}
}
}
return lRecNum;
}
int32 CBofDataFile::getRecSize(int32 lRecNum) {
assert(isValidObject(this));
int32 lSize = -1;
// Only continue if there is no current error
if (_errCode == ERR_NONE) {
// Validate record number
assert(lRecNum >= 0 && lRecNum < _lNumRecs);
assert(_pHeader != nullptr);
lSize = _pHeader[lRecNum]._lLength;
}
return lSize;
}
int32 CBofDataFile::getMaxRecSize() const {
assert(isValidObject(this));
int32 lLargest = -1;
// Only continue if there is no current error
if (_errCode == ERR_NONE) {
// Validate header
assert(_pHeader != nullptr);
for (int i = 0; i < (int)_lNumRecs; i++) {
lLargest = MAX(lLargest, _pHeader[i]._lLength);
}
}
return lLargest;
}
void CBofDataFile::setPassword(const char *pszPassword) {
assert(isValidObject(this));
_szPassWord[0] = '\0';
if (pszPassword != nullptr) {
assert(strlen(pszPassword) < MAX_PW_LEN);
Common::strcpy_s(_szPassWord, pszPassword);
}
}
ErrorCode CBofDataFile::read(void *pDestBuf, int32 lBytes) {
return CBofFile::read(pDestBuf, lBytes);
}
ErrorCode CBofDataFile::read(HeadInfo &rec) {
byte buf[16];
ErrorCode errorCode = read(&buf[0], 16);
Common::MemoryReadStream mem(buf, 16);
Common::Serializer s(&mem, nullptr);
rec.synchronize(s);
return errorCode;
}
ErrorCode CBofDataFile::read(HeaderRec &rec) {
byte buf[16];
ErrorCode errorCode = read(&buf[0], 16);
Common::MemoryReadStream mem(buf, 16);
Common::Serializer s(&mem, nullptr);
rec.synchronize(s);
return errorCode;
}
ErrorCode CBofDataFile::write(const void *pSrcBuf, int32 lBytes) {
return CBofFile::write(pSrcBuf, lBytes);
}
ErrorCode CBofDataFile::write(HeadInfo &rec) {
byte buf[16];
Common::MemoryWriteStream mem(buf, 16);
Common::Serializer s(nullptr, &mem);
rec.synchronize(s);
return write(&buf[0], 16);
}
ErrorCode CBofDataFile::write(HeaderRec &rec) {
byte buf[16];
Common::MemoryWriteStream mem(buf, 16);
Common::Serializer s(nullptr, &mem);
rec.synchronize(s);
return write(&buf[0], 16);
}
/**
* Builds a Hash code based on a key.
* @param pKey Key
* @return Hash code
*/
uint32 CreateHashCode(const byte *pKey) {
// validate input
assert(pKey != nullptr);
uint32 lCode = ((uint32)*pKey << 24) | ((uint32)*(pKey + 1) << 16) | ((uint32)*(pKey + 2) << 8) | *(pKey + 3);
return lCode;
}
void SwapHeadInfo(HeadInfo *stHI) {
// Macintosh is big endian, so we must swap our bytes
stHI->_lNumRecs = SWAPLONG(stHI->_lNumRecs);
stHI->_lAddress = SWAPLONG(stHI->_lAddress);
stHI->_lFlags = SWAPLONG(stHI->_lFlags);
stHI->_lFootCrc = SWAPLONG(stHI->_lFootCrc);
}
void SwapHeaderRec(HeaderRec *stHR, int nRecords) {
HeaderRec *p = stHR;
for (int i = 0; i < nRecords; i++) {
p->_lOffset = SWAPLONG(p->_lOffset);
p->_lLength = SWAPLONG(p->_lLength);
p->_lCrc = SWAPLONG(p->_lCrc);
p->_lKey = SWAPLONG(p->_lKey);
p++;
}
}
} // namespace Bagel