mirror of
https://github.com/scummvm/scummvm.git
synced 2026-06-20 05:45:29 +00:00
zlib is used when available and falls back on gzio. This allows performance improvements as our CRC32 and gzio implementations are slower than base zlib. As zlib is available when libpng is present, this is sensible to benefit from it.
301 lines
8.1 KiB
C++
301 lines
8.1 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 "engines/grim/update/mscab.h"
|
|
|
|
#include "common/file.h"
|
|
#include "common/archive.h"
|
|
#include "common/memstream.h"
|
|
#include "common/compression/deflate.h"
|
|
#include "common/str.h"
|
|
|
|
namespace Grim {
|
|
|
|
MsCabinet::~MsCabinet() {
|
|
for (CacheMap::iterator it = _cache.begin(); it != _cache.end(); it++)
|
|
delete[] it->_value;
|
|
|
|
_folderMap.clear();
|
|
_fileMap.clear();
|
|
|
|
delete _data;
|
|
|
|
delete _decompressor;
|
|
}
|
|
|
|
MsCabinet::MsCabinet(Common::SeekableReadStream *data) :
|
|
_data(data), _decompressor(nullptr) {
|
|
if (!_data)
|
|
return;
|
|
|
|
// CFHEADER PARSING
|
|
|
|
// Verify Head-signature
|
|
uint32 tag = _data->readUint32BE();
|
|
if (tag != MKTAG('M','S','C','F'))
|
|
return;
|
|
|
|
/* uint32 reserved1 = */ _data->readUint32LE();
|
|
uint32 length = _data->readUint32LE();
|
|
if (length > uint32(_data->size()))
|
|
return;
|
|
|
|
/* uint32 reserved2 = */ _data->readUint32LE();
|
|
uint32 filesOffset = _data->readUint32LE();
|
|
/* uint32 reserved3 = */ _data->readUint32LE();
|
|
|
|
byte versionMinor = _data->readByte();
|
|
byte versionMajor = _data->readByte();
|
|
if (versionMajor != 1 || versionMinor != 3)
|
|
return;
|
|
|
|
uint16 numFolders = _data->readUint16LE();
|
|
uint16 numFiles = _data->readUint16LE();
|
|
if (numFolders == 0 || numFiles == 0)
|
|
return;
|
|
|
|
// This implementation doesn't support multicabinet and reserved fields
|
|
uint16 flags = _data->readUint16LE();
|
|
if (flags != 0)
|
|
return;
|
|
|
|
/* uint16 setId = */ _data->readUint16LE();
|
|
/* uint16 iCab = */ _data->readUint16LE();
|
|
|
|
if (_data->err())
|
|
return;
|
|
|
|
//CFFOLDERs PARSING
|
|
for (uint16 i = 0; i < numFolders; ++i) {
|
|
FolderEntry fEntry;
|
|
|
|
fEntry.offset = _data->readUint32LE();
|
|
fEntry.num_blocks = _data->readUint16LE();
|
|
fEntry.comp_type = _data->readUint16LE();
|
|
|
|
if (_data->err())
|
|
return;
|
|
|
|
_folderMap[i] = fEntry;
|
|
}
|
|
|
|
// CFFILEs PARSING
|
|
_data->seek(filesOffset);
|
|
if (_data->err())
|
|
return;
|
|
|
|
for (uint16 i = 0; i < numFiles; ++i) {
|
|
FileEntry fEntry;
|
|
|
|
fEntry.length = _data->readUint32LE();
|
|
fEntry.folderOffset = _data->readUint32LE();
|
|
uint16 iFolder = _data->readUint16LE();
|
|
/* uint16 date = */ _data->readUint16LE();
|
|
/* uint16 time = */ _data->readUint16LE();
|
|
/* uint16 attribs = */ _data->readUint16LE();
|
|
Common::String name = readString(_data);
|
|
for (uint l = 0; l < name.size(); ++l)
|
|
if (name[l] == '\\')
|
|
name.setChar('/', l);
|
|
|
|
if (_data->err()) {
|
|
_fileMap.clear();
|
|
return;
|
|
}
|
|
|
|
if (_folderMap.contains(iFolder))
|
|
fEntry.folder = &_folderMap[iFolder];
|
|
else {
|
|
_fileMap.clear();
|
|
return;
|
|
}
|
|
|
|
_fileMap[name] = fEntry;
|
|
}
|
|
}
|
|
|
|
/* read a null-terminated string from a stream
|
|
Copied from ScummVm MohawkEngine_LivingBooks.*/
|
|
Common::String MsCabinet::readString(Common::ReadStream *stream) {
|
|
Common::String ret;
|
|
while (!stream->eos()) {
|
|
byte in = stream->readByte();
|
|
if (!in)
|
|
break;
|
|
ret += in;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
bool MsCabinet::hasFile(const Common::Path &path) const {
|
|
Common::String name = path.toString();
|
|
return _fileMap.contains(name);
|
|
}
|
|
|
|
int MsCabinet::listMembers(Common::ArchiveMemberList &list) const {
|
|
for (FileMap::const_iterator it = _fileMap.begin(); it != _fileMap.end(); it++)
|
|
list.push_back(getMember(it->_key));
|
|
|
|
return _fileMap.size();
|
|
}
|
|
|
|
const Common::ArchiveMemberPtr MsCabinet::getMember(const Common::Path &path) const {
|
|
Common::String name = path.toString();
|
|
return Common::ArchiveMemberPtr(new Common::GenericArchiveMember(name, *this));
|
|
}
|
|
|
|
Common::SeekableReadStream *MsCabinet::createReadStreamForMember(const Common::Path &path) const {
|
|
Common::String name = path.toString();
|
|
byte *fileBuf;
|
|
|
|
if (!hasFile(name))
|
|
return nullptr;
|
|
|
|
const FileEntry &entry = _fileMap[name];
|
|
|
|
// Check if the file has already been decompressed and it's in the cache,
|
|
// otherwise decompress it and put it in the cache
|
|
if (_cache.contains(name))
|
|
fileBuf = _cache[name];
|
|
else {
|
|
// Check if the decompressor should be reinitialized
|
|
if (!_decompressor || entry.folder != _decompressor->getFolder()) {
|
|
delete _decompressor;
|
|
|
|
_decompressor = new Decompressor(entry.folder, _data);
|
|
}
|
|
|
|
if (!_decompressor->decompressFile(fileBuf, entry))
|
|
return nullptr;
|
|
|
|
_cache[name] = fileBuf;
|
|
}
|
|
|
|
return new Common::MemoryReadStream(fileBuf, entry.length, DisposeAfterUse::NO);
|
|
}
|
|
|
|
MsCabinet::Decompressor::Decompressor(const MsCabinet::FolderEntry *folder, Common::SeekableReadStream *data) :
|
|
_curFolder(folder), _data(data), _curBlock(-1), _compressedBlock(nullptr), _decompressedBlock(nullptr), _fileBuf(nullptr),
|
|
_inBlockEnd(0), _inBlockStart(0), _endBlock(0), _startBlock(0) {
|
|
|
|
// Alloc the decompression buffers
|
|
_compressedBlock = new byte[kCabInputmax];
|
|
_decompressedBlock = new byte[kCabBlockSize];
|
|
}
|
|
|
|
MsCabinet::Decompressor::~Decompressor() {
|
|
delete[] _decompressedBlock;
|
|
|
|
delete[] _compressedBlock;
|
|
|
|
delete[] _fileBuf;
|
|
}
|
|
|
|
bool MsCabinet::Decompressor::decompressFile(byte *&fileBuf, const FileEntry &entry) {
|
|
// Ref: http://blogs.kde.org/node/3181
|
|
uint16 uncompressedLen, compressedLen;
|
|
byte hdrS[4];
|
|
byte *buf_tmp, *dict;
|
|
bool decRes;
|
|
|
|
// Sanity checks
|
|
if (!_compressedBlock || entry.folder != _curFolder)
|
|
return false;
|
|
|
|
_startBlock = entry.folderOffset / kCabBlockSize;
|
|
_inBlockStart = entry.folderOffset % kCabBlockSize;
|
|
_endBlock = (entry.folderOffset + entry.length) / kCabBlockSize;
|
|
_inBlockEnd = (entry.folderOffset + entry.length) % kCabBlockSize;
|
|
|
|
// Check if the decompressor should be reinitialized
|
|
if (_curBlock > _startBlock || _curBlock == -1) {
|
|
_data->seek(entry.folder->offset);
|
|
// Check the compression method (only mszip supported)
|
|
if (entry.folder->comp_type != kMszipCompression)
|
|
return false;
|
|
|
|
_curBlock = -1; // No block decompressed
|
|
}
|
|
|
|
// Check if the file is contained in the folder
|
|
if ((entry.length + entry.folderOffset) / kCabBlockSize > entry.folder->num_blocks)
|
|
return false;
|
|
|
|
_fileBuf = new byte[entry.length];
|
|
|
|
buf_tmp = _fileBuf;
|
|
|
|
// If a part of this file has been decompressed in the last block, make a copy of it
|
|
copyBlock(buf_tmp);
|
|
|
|
while ((_curBlock + 1) <= _endBlock) {
|
|
// Read the CFDATA header
|
|
_data->readUint32LE(); // checksum
|
|
_data->read(hdrS, 4);
|
|
compressedLen = READ_LE_UINT16(hdrS);
|
|
uncompressedLen = READ_LE_UINT16(hdrS + 2);
|
|
|
|
if (_data->err())
|
|
return false;
|
|
|
|
if (compressedLen > kCabInputmax || uncompressedLen > kCabBlockSize)
|
|
return false;
|
|
|
|
// Read the compressed block
|
|
if (_data->read(_compressedBlock, compressedLen) != compressedLen)
|
|
return false;
|
|
|
|
// Check the CK header
|
|
if (_compressedBlock[0] != 'C' || _compressedBlock[1] != 'K')
|
|
return false;
|
|
|
|
// Decompress the block. If it isn't the first, provide the previous block as dictonary
|
|
dict = (_curBlock >= 0) ? _decompressedBlock : nullptr;
|
|
decRes = Common::inflateZlibHeaderless(_decompressedBlock, uncompressedLen, _compressedBlock + 2, compressedLen - 2, dict, kCabBlockSize);
|
|
if (!decRes)
|
|
return false;
|
|
|
|
_curBlock++;
|
|
|
|
// Copy the decompressed data, if needed
|
|
copyBlock(buf_tmp);
|
|
}
|
|
|
|
fileBuf = _fileBuf;
|
|
_fileBuf = nullptr;
|
|
return true;
|
|
}
|
|
|
|
void MsCabinet::Decompressor::copyBlock(byte *&data_ptr) const {
|
|
uint16 start, end, size;
|
|
|
|
if (_startBlock <= _curBlock && _curBlock <= _endBlock) {
|
|
start = (_startBlock == _curBlock) ? _inBlockStart : 0;
|
|
end = (_endBlock == _curBlock) ? _inBlockEnd : uint16(kCabBlockSize);
|
|
size = end - start;
|
|
|
|
memcpy(data_ptr, _decompressedBlock + start, size);
|
|
data_ptr += size;
|
|
}
|
|
}
|
|
|
|
} // End of namespace Grim
|