613 lines
15 KiB
C++
613 lines
15 KiB
C++
// Copyright 2003 Jeremy Sawicki
|
|
//
|
|
// This file is part of OxydLib.
|
|
//
|
|
// OxydLib 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 2 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// OxydLib 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 OxydLib; if not, write to the Free Software
|
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
#include "DatFile.h"
|
|
#include "VecUtils.h"
|
|
#include <stdio.h>
|
|
#include <algorithm>
|
|
|
|
namespace OxydLib {
|
|
|
|
using namespace std;
|
|
|
|
namespace
|
|
{
|
|
unsigned char levelShifts[OxydVersion_Count][16] =
|
|
{{0x45, 0x23, 0xEA, 0xB9, 0x11, 0xF1, 0x9F, 0x5A,
|
|
0x33, 0x3E, 0x0F, 0xB4, 0x41, 0x56, 0xAF, 0xAA},
|
|
|
|
{0x45, 0x23, 0xEA, 0xB9, 0x11, 0xF1, 0x9F, 0x5A,
|
|
0x33, 0x3E, 0x0F, 0xB4, 0x41, 0x56, 0xAF, 0xAA},
|
|
|
|
{0x45, 0x23, 0xEA, 0xB9, 0x11, 0xF1, 0x9F, 0x5A,
|
|
0x33, 0x3E, 0x0F, 0xB4, 0x41, 0x56, 0xAF, 0xAA},
|
|
|
|
{0xAA, 0xE1, 0x82, 0x34, 0xAF, 0x24, 0x72, 0x42,
|
|
0x37, 0x3E, 0x83, 0x98, 0x22, 0xFF, 0x2F, 0x31},
|
|
|
|
{0x45, 0x23, 0xEA, 0xB9, 0x11, 0xF1, 0x9F, 0x5A,
|
|
0x33, 0x3E, 0x0F, 0xB4, 0x41, 0x56, 0xAF, 0xAA}};
|
|
|
|
void unshiftBytes(ByteVec *pVec, OxydVersion ver)
|
|
{
|
|
for (int nByte = 0; nByte < (int)pVec->size(); nByte++) {
|
|
(*pVec)[nByte] =
|
|
(int((*pVec)[nByte]) + 256 - levelShifts[ver][nByte % 16]) % 256;
|
|
}
|
|
}
|
|
|
|
void shiftBytes(ByteVec *pVec, OxydVersion ver)
|
|
{
|
|
for (int nByte = 0; nByte < (int)pVec->size(); nByte++) {
|
|
(*pVec)[nByte] =
|
|
(int((*pVec)[nByte]) + levelShifts[ver][nByte % 16]) % 256;
|
|
}
|
|
}
|
|
|
|
void unscrambleChunkTableData(ByteVec::iterator it)
|
|
{
|
|
unsigned int val = getInt4(it);
|
|
unsigned int valPrime = 0xffffffff &
|
|
~(((val & 0x1fffffff) << 3) +
|
|
((val & 0xe0000000) >> 29));
|
|
putInt4(it, valPrime);
|
|
}
|
|
|
|
void scrambleChunkTableData(ByteVec::iterator it)
|
|
{
|
|
unsigned int val = getInt4(it);
|
|
unsigned int valPrime = 0xffffffff &
|
|
~(((val & 0xfffffff8) >> 3) +
|
|
((val & 0x00000007) << 29));
|
|
putInt4(it, valPrime);
|
|
}
|
|
}
|
|
|
|
DatFile::DatFile()
|
|
{
|
|
clear();
|
|
}
|
|
|
|
DatFile::~DatFile()
|
|
{
|
|
}
|
|
|
|
void DatFile::clear()
|
|
{
|
|
m_bInit = false;
|
|
|
|
m_ver = OxydVersion_Oxyd1;
|
|
m_credits.clear();
|
|
m_codes.clear();
|
|
m_levels.clear();
|
|
m_levels.resize(200);
|
|
m_chunks.clear();
|
|
}
|
|
|
|
void DatFile::getChunkNames(set<string> *pChunkNames) const
|
|
{
|
|
pChunkNames->clear();
|
|
|
|
ChunkMap::const_iterator iter = m_chunks.begin();
|
|
ChunkMap::const_iterator end = m_chunks.end();
|
|
for (; iter != end; ++iter) {
|
|
pChunkNames->insert(iter->first);
|
|
}
|
|
}
|
|
|
|
void DatFile::addChunk(const string &chunkName)
|
|
{
|
|
m_chunks[chunkName];
|
|
}
|
|
|
|
void DatFile::removeChunk(const string &chunkName)
|
|
{
|
|
m_chunks.erase(chunkName);
|
|
}
|
|
|
|
const ByteVec *DatFile::getChunk(const string &chunkName) const
|
|
{
|
|
ChunkMap::const_iterator iter = m_chunks.find(chunkName);
|
|
if (iter == m_chunks.end()) {
|
|
return 0;
|
|
} else {
|
|
return &iter->second;
|
|
}
|
|
}
|
|
|
|
ByteVec *DatFile::getChunkForWrite(const string &chunkName)
|
|
{
|
|
ChunkMap::iterator iter = m_chunks.find(chunkName);
|
|
if (iter == m_chunks.end()) {
|
|
return 0;
|
|
} else {
|
|
return &iter->second;
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
bool uncompressChunk(const ByteVec &in,
|
|
ByteVec *pOut,
|
|
string *pMsg)
|
|
{
|
|
string msg;
|
|
if (!pMsg) {
|
|
pMsg = &msg;
|
|
}
|
|
|
|
ByteVec &out = *pOut;
|
|
|
|
int compressedSize = in.size();
|
|
int uncompressedSize = out.size();
|
|
|
|
if (compressedSize == uncompressedSize) {
|
|
copy(in.begin(), in.end(), out.begin());
|
|
} else {
|
|
unsigned char keyByte = 0;
|
|
int curBit = 0;
|
|
int curIn = 0;
|
|
int curOut = 0;
|
|
int szIn = in.size();
|
|
int szOut = out.size();
|
|
|
|
while (curOut < szOut) {
|
|
if (curBit == 0) {
|
|
if (curIn + 1 > szIn) {
|
|
pMsg->assign("Not enough compressed data reading key byte.");
|
|
return false;
|
|
}
|
|
|
|
keyByte = in[curIn];
|
|
curIn++;
|
|
}
|
|
|
|
if (keyByte & (1 << curBit)) {
|
|
if (curIn + 1 > szIn) {
|
|
pMsg->assign("Not enough compressed data reading literal byte.");
|
|
return false;
|
|
}
|
|
|
|
unsigned char literalByte = in[curIn];
|
|
curIn++;
|
|
|
|
out[curOut] = literalByte;
|
|
curOut++;
|
|
} else {
|
|
if (curIn + 2 > szIn) {
|
|
pMsg->assign("Not enough compressed data reading compressed byte.");
|
|
return false;
|
|
}
|
|
|
|
unsigned char b1 = in[curIn];
|
|
curIn++;
|
|
unsigned char b2 = in[curIn];
|
|
curIn++;
|
|
|
|
int outBytes = ((int)(b2 & 0x0f)) + 3;
|
|
|
|
if (curOut + outBytes > szOut) {
|
|
pMsg->assign("Too much uncompressed data.");
|
|
return false;
|
|
}
|
|
|
|
int offset = ((int)b1) + ((((int)b2) & 0xf0) << 4);
|
|
|
|
offset = ((offset + 0x12) & 0x0fff);
|
|
|
|
offset |= (curOut & ~0x0fff);
|
|
|
|
if (offset >= curOut) {
|
|
offset -= 0x1000;
|
|
}
|
|
|
|
if (offset + outBytes < 0) {
|
|
pMsg->assign("Referencing data unnecessarily far before beginning of output.");
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < outBytes; i++) {
|
|
if (offset < 0) {
|
|
out[curOut] = 0x20;
|
|
} else {
|
|
out[curOut] = out[offset];
|
|
}
|
|
curOut++;
|
|
offset++;
|
|
}
|
|
}
|
|
|
|
curBit = ((curBit + 1) & 0x7);
|
|
}
|
|
|
|
if (curIn < szIn) {
|
|
pMsg->assign("Too much compressed data while uncompressing chunk.");
|
|
return false;
|
|
}
|
|
|
|
while (curBit != 0) {
|
|
if (keyByte & (1 << curBit)) {
|
|
pMsg->assign("Extra bit in compressed chunk key byte is not 0.");
|
|
return false;
|
|
}
|
|
|
|
curBit = ((curBit + 1) & 0x7);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool parseDatFile(const ByteVec &in,
|
|
OxydVersion ver,
|
|
DatFile *pDatFile,
|
|
string *pMsg)
|
|
{
|
|
string msg;
|
|
if (!pMsg) {
|
|
pMsg = &msg;
|
|
}
|
|
|
|
pDatFile->clear();
|
|
|
|
pDatFile->setVersion(ver);
|
|
|
|
int sz = in.size();
|
|
int cur = 0;
|
|
|
|
if (cur + 600 + 4 + 4 + 400 > sz) {
|
|
pMsg->assign("File too small for header.");
|
|
return false;
|
|
}
|
|
|
|
ByteVec *pCredits = pDatFile->getCreditsForWrite();
|
|
pCredits->resize(600);
|
|
copy(in.begin() + cur, in.begin() + cur + 600, pCredits->begin());
|
|
cur += 600;
|
|
|
|
int levelSkip = getInt4(in.begin() + cur);
|
|
cur += 4;
|
|
if (levelSkip < (4 + 4 + 400)) {
|
|
pMsg->assign("Level skip value is too small.");
|
|
return false;
|
|
}
|
|
|
|
ByteVec *pCodes = pDatFile->getCodesForWrite();
|
|
pCodes->resize(4);
|
|
copy(in.begin() + cur, in.begin() + cur + 4, pCodes->begin());
|
|
cur += 4;
|
|
|
|
vector<int> levelSizes;
|
|
levelSizes.resize(200);
|
|
int levelSizeTotal = 0;
|
|
|
|
int nLevel;
|
|
for (nLevel = 0; nLevel < 200; nLevel++) {
|
|
int levelSize = getInt2(in.begin() + cur);
|
|
cur += 2;
|
|
levelSizes[nLevel] = levelSize;
|
|
levelSizeTotal += levelSize;
|
|
}
|
|
|
|
if (levelSkip != 4 + 4 + 400 + levelSizeTotal) {
|
|
pMsg->assign("Level skip value does not correspond to sum of level sizes.");
|
|
return false;
|
|
}
|
|
|
|
if (cur + levelSizeTotal > sz) {
|
|
pMsg->assign("File too small for levels.");
|
|
return false;
|
|
}
|
|
|
|
for (nLevel = 0; nLevel < 200; nLevel++) {
|
|
ByteVec *pLevelData = pDatFile->getLevelForWrite(nLevel);
|
|
pLevelData->resize(levelSizes[nLevel]);
|
|
copy(in.begin() + cur,
|
|
in.begin() + cur + levelSizes[nLevel],
|
|
pLevelData->begin());
|
|
unshiftBytes(pLevelData, ver);
|
|
cur += levelSizes[nLevel];
|
|
}
|
|
|
|
// Chunks
|
|
if (cur + 2 > sz) {
|
|
pMsg->assign("Not enough data reading number of chunks.");
|
|
return false;
|
|
}
|
|
|
|
int numChunks = getInt2(in.begin() + cur);
|
|
cur += 2;
|
|
|
|
if (cur + (24 * numChunks) > sz) {
|
|
pMsg->assign("Not enough data reading chunk table.");
|
|
return false;
|
|
}
|
|
|
|
vector<int> chunkCompressedSizes, chunkUncompressedSizes, chunkOffsets;
|
|
vector<string> chunkNames;
|
|
chunkCompressedSizes.resize(numChunks);
|
|
chunkUncompressedSizes.resize(numChunks);
|
|
chunkOffsets.resize(numChunks);
|
|
chunkNames.resize(numChunks);
|
|
|
|
int nChunk;
|
|
for (nChunk = 0; nChunk < numChunks; nChunk++) {
|
|
ByteVec chunkTableEntry;
|
|
chunkTableEntry.resize(24);
|
|
copy(in.begin() + cur, in.begin() + cur + 24,
|
|
chunkTableEntry.begin());
|
|
cur += 24;
|
|
|
|
for (int i = 0; i < 24; i += 4) {
|
|
unscrambleChunkTableData(chunkTableEntry.begin() + i);
|
|
}
|
|
|
|
chunkCompressedSizes[nChunk] = getInt4(chunkTableEntry.begin() + 0);
|
|
chunkUncompressedSizes[nChunk] = getInt4(chunkTableEntry.begin() + 4);
|
|
chunkOffsets[nChunk] = getInt4(chunkTableEntry.begin() + 8);
|
|
|
|
string &chunkName = chunkNames[nChunk];
|
|
for (int nChar = 0; nChar < 12; nChar++) {
|
|
char ch = chunkTableEntry[12 + nChar];
|
|
if ((int)chunkName.size() == nChar) {
|
|
if (ch != 0x00) {
|
|
chunkName.append(1, ch);
|
|
}
|
|
} else {
|
|
if (ch != 0x00) {
|
|
pMsg->assign("Chunk name has non-null byte after null byte.");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pDatFile->getChunk(chunkName)) {
|
|
pMsg->assign("Duplicate chunk name.");
|
|
return false;
|
|
}
|
|
|
|
pDatFile->addChunk(chunkName);
|
|
}
|
|
|
|
int chunkOffset = 2 + (24 * numChunks);
|
|
|
|
for (nChunk = 0; nChunk < numChunks; nChunk++) {
|
|
if (chunkOffset != chunkOffsets[nChunk]) {
|
|
pMsg->assign("Unexpected chunk starting point.");
|
|
return false;
|
|
}
|
|
|
|
int compressedSize = chunkCompressedSizes[nChunk];
|
|
if (cur + compressedSize > sz) {
|
|
pMsg->assign("Not enough data reading chunk.");
|
|
return false;
|
|
}
|
|
|
|
ByteVec compressedChunk;
|
|
compressedChunk.resize(compressedSize);
|
|
copy(in.begin() + cur, in.begin() + cur + compressedSize,
|
|
compressedChunk.begin());
|
|
cur += compressedSize;
|
|
|
|
chunkOffset += compressedSize;
|
|
|
|
const string &chunkName = chunkNames[nChunk];
|
|
int uncompressedSize = chunkUncompressedSizes[nChunk];
|
|
ByteVec &uncompressedChunk = *pDatFile->getChunkForWrite(chunkName);
|
|
uncompressedChunk.resize(uncompressedSize);
|
|
|
|
if (!uncompressChunk(compressedChunk, &uncompressedChunk, pMsg)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (cur < sz) {
|
|
pMsg->assign("Extra data after reading chunks.");
|
|
return false;
|
|
}
|
|
|
|
pDatFile->setInit(true);
|
|
return true;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
bool compressChunk(const ByteVec &in,
|
|
ByteVec *pOut,
|
|
string *pMsg)
|
|
{
|
|
string msg;
|
|
if (!pMsg) {
|
|
pMsg = &msg;
|
|
}
|
|
|
|
ByteVec out;
|
|
|
|
// We could do some real compression here instead.
|
|
out.resize(in.size());
|
|
copy(in.begin(), in.end(), out.begin());
|
|
|
|
pOut->resize(out.size());
|
|
copy(out.begin(), out.end(), pOut->begin());
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool unparseDatFile(const DatFile &datFile,
|
|
ByteVec *pOut,
|
|
string *pMsg)
|
|
{
|
|
string msg;
|
|
if (!pMsg) {
|
|
pMsg = &msg;
|
|
}
|
|
|
|
if (!datFile.getInit()) {
|
|
pMsg->assign("DatFile not initialized.");
|
|
return false;
|
|
}
|
|
|
|
ByteVec out;
|
|
|
|
out.clear();
|
|
|
|
int cur = 0;
|
|
|
|
out.resize(out.size() + 600 + 4 + 4 + 400);
|
|
|
|
const ByteVec &credits = datFile.getCredits();
|
|
if (credits.size() != 600) {
|
|
pMsg->assign("Credits must be 600 bytes.");
|
|
return false;
|
|
}
|
|
copy(credits.begin(), credits.end(), out.begin() + cur);
|
|
cur += 600;
|
|
|
|
int levelSizeTotal = 0;
|
|
int nLevel;
|
|
for (nLevel = 0; nLevel < 200; nLevel++) {
|
|
levelSizeTotal += datFile.getLevel(nLevel).size();
|
|
}
|
|
|
|
putInt4(out.begin() + cur, 4 + 4 + 400 + levelSizeTotal);
|
|
cur += 4;
|
|
|
|
const ByteVec &codes = datFile.getCodes();
|
|
if (codes.size() != 4) {
|
|
pMsg->assign("Codes must be 4 bytes.");
|
|
return false;
|
|
}
|
|
copy(codes.begin(), codes.end(), out.begin() + cur);
|
|
cur += 4;
|
|
|
|
for (nLevel = 0; nLevel < 200; nLevel++) {
|
|
putInt2(out.begin() + cur, datFile.getLevel(nLevel).size());
|
|
cur += 2;
|
|
}
|
|
|
|
out.resize(out.size() + levelSizeTotal);
|
|
|
|
for (nLevel = 0; nLevel < 200; nLevel++) {
|
|
const ByteVec &level = datFile.getLevel(nLevel);
|
|
ByteVec encodedLevel;
|
|
encodedLevel.resize(level.size());
|
|
copy(level.begin(), level.end(),
|
|
encodedLevel.begin());
|
|
shiftBytes(&encodedLevel, datFile.getVersion());
|
|
copy(encodedLevel.begin(), encodedLevel.end(),
|
|
out.begin() + cur);
|
|
cur += level.size();
|
|
}
|
|
|
|
// Chunks
|
|
set<string> chunkNameSet;
|
|
datFile.getChunkNames(&chunkNameSet);
|
|
|
|
int numChunks = chunkNameSet.size();
|
|
|
|
vector<string> chunkNames;
|
|
vector<ByteVec> compressedChunks;
|
|
chunkNames.resize(numChunks);
|
|
compressedChunks.resize(numChunks);
|
|
|
|
int nChunk = 0;
|
|
set<string>::const_iterator chunkIter = chunkNameSet.begin();
|
|
set<string>::const_iterator chunkEnd = chunkNameSet.end();
|
|
for (; chunkIter != chunkEnd; ++chunkIter) {
|
|
const string &chunkName = *chunkIter;
|
|
chunkNames[nChunk] = chunkName;
|
|
|
|
if (!compressChunk(*datFile.getChunk(chunkName),
|
|
&compressedChunks[nChunk],
|
|
pMsg)) {
|
|
return false;
|
|
}
|
|
|
|
nChunk++;
|
|
}
|
|
|
|
out.resize(out.size() + 2 + (24 * numChunks));
|
|
|
|
putInt2(out.begin() + cur, numChunks);
|
|
cur += 2;
|
|
|
|
int chunkOffset = 2 + (24 * numChunks);
|
|
|
|
for (nChunk = 0; nChunk < numChunks; nChunk++) {
|
|
ByteVec chunkTableEntry;
|
|
chunkTableEntry.resize(24);
|
|
|
|
const string &chunkName = chunkNames[nChunk];
|
|
|
|
int uncompressedSize = datFile.getChunk(chunkName)->size();
|
|
int compressedSize = compressedChunks[nChunk].size();
|
|
|
|
putInt4(chunkTableEntry.begin(), compressedSize);
|
|
putInt4(chunkTableEntry.begin() + 4, uncompressedSize);
|
|
putInt4(chunkTableEntry.begin() + 8, chunkOffset);
|
|
|
|
if (chunkName.size() > 12) {
|
|
pMsg->assign("Chunk name too long.");
|
|
return false;
|
|
}
|
|
for (int nChar = 0; nChar < 12; nChar++) {
|
|
char ch = 0x00;
|
|
if (nChar < (int)chunkName.size()) {
|
|
ch = chunkName[nChar];
|
|
}
|
|
chunkTableEntry[12 + nChar] = ch;
|
|
}
|
|
|
|
for (int i = 0; i < 24; i += 4) {
|
|
scrambleChunkTableData(chunkTableEntry.begin() + i);
|
|
}
|
|
|
|
copy(chunkTableEntry.begin(), chunkTableEntry.end(), out.begin() + cur);
|
|
cur += 24;
|
|
|
|
chunkOffset += compressedSize;
|
|
}
|
|
|
|
for (nChunk = 0; nChunk < numChunks; nChunk++) {
|
|
const ByteVec &compressedChunk = compressedChunks[nChunk];
|
|
int compressedSize = compressedChunk.size();
|
|
|
|
out.resize(out.size() + compressedSize);
|
|
|
|
copy(compressedChunk.begin(),
|
|
compressedChunk.end(),
|
|
out.begin() + cur);
|
|
cur += compressedSize;
|
|
}
|
|
|
|
if (cur != (int)out.size()) {
|
|
pMsg->assign("Error computing size of unparsed data file.");
|
|
return false;
|
|
}
|
|
|
|
pOut->clear();
|
|
pOut->resize(out.size());
|
|
copy(out.begin(), out.end(), pOut->begin());
|
|
|
|
return true;
|
|
}
|
|
|
|
}
|