/* 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 "bagel/spacebar/boflib/dat_file.h" #include "bagel/spacebar/boflib/crc.h" #include "bagel/spacebar/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 { namespace SpaceBar { // 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(_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 SpaceBar } // namespace Bagel