#include "codecs/compression.h"
#include "crypto.h"
#include "writer.h"
#include <whiteout/interfaces.h>
#include <whiteout/utils/job_group.h>
#include <algorithm>
#include <chrono>
#include <cstring>
namespace whiteout::storages::mpq {
namespace {
BlockEntry makeBlockEntry(u32 offset, const EncodedFile& encoded, u32 uncompressedSize) {
BlockEntry be;
be.fileOffset = offset;
be.compressedSize = encoded.compressedSize;
be.uncompressedSize = uncompressedSize;
be.flags = encoded.flags;
return be;
}
BlockEntry makeRawCopyBlockEntry(u32 newOffset, const BlockEntry& source) {
BlockEntry be = source;
be.fileOffset = newOffset;
return be;
}
u64 currentFiletime() {
auto now = std::chrono::system_clock::now();
auto duration = now.time_since_epoch();
auto hundred_ns =
std::chrono::duration_cast<std::chrono::duration<u64, std::ratio<1, 10000000>>>(duration);
constexpr u64 kWindowsEpochOffset = 116444736000000000ULL;
return hundred_ns.count() + kWindowsEpochOffset;
}
void serializeHeader(std::vector<u8>& archive, const MpqHeader& hdr) {
if (archive.size() < 32) {
archive.resize(32, 0);
}
static constexpr size_t kMagic = 0;
static constexpr size_t kHeaderSize = 4;
static constexpr size_t kArchiveSize = 8;
static constexpr size_t kFormatVersion = 12;
static constexpr size_t kSectorSizeShift = 14;
static constexpr size_t kHashTableOffset = 16;
static constexpr size_t kBlockTableOffset = 20;
static constexpr size_t kHashTableEntries = 24;
static constexpr size_t kBlockTableEntries = 28;
static constexpr size_t kHiBlockTableOffset = 32;
static constexpr size_t kHashTableOffsetHi = 40;
static constexpr size_t kBlockTableOffsetHi = 42;
static constexpr size_t kArchiveSize64 = 44;
static constexpr size_t kBetTableOffset = 52;
static constexpr size_t kHetTableOffset = 60;
static constexpr size_t kHashTableSize64 = 68;
static constexpr size_t kBlockTableSize64 = 76;
static constexpr size_t kHiBlockTableSize64 = 84;
static constexpr size_t kHetTableSize64 = 92;
static constexpr size_t kBetTableSize64 = 100;
static constexpr size_t kRawChunkSize = 108;
static constexpr size_t kBlockTableMd5 = 112;
static constexpr size_t kHashTableMd5 = 128;
static constexpr size_t kHiBlockTableMd5 = 144;
static constexpr size_t kBetTableMd5 = 160;
static constexpr size_t kHetTableMd5 = 176;
static constexpr size_t kMpqHeaderMd5 = 192;
auto w = [&](size_t off, const void* src, size_t len) {
std::memcpy(archive.data() + off, src, len);
};
w(kMagic, &hdr.magic, 4);
w(kHeaderSize, &hdr.headerSize, 4);
w(kArchiveSize, &hdr.archiveSize, 4);
w(kFormatVersion, &hdr.formatVersion, 2);
w(kSectorSizeShift, &hdr.sectorSizeShift, 2);
w(kHashTableOffset, &hdr.hashTableOffset, 4);
w(kBlockTableOffset, &hdr.blockTableOffset, 4);
w(kHashTableEntries, &hdr.hashTableEntries, 4);
w(kBlockTableEntries, &hdr.blockTableEntries, 4);
if (hdr.headerSize >= 44) {
w(kHiBlockTableOffset, &hdr.hiBlockTableOffset, 8);
w(kHashTableOffsetHi, &hdr.hashTableOffsetHi, 2);
w(kBlockTableOffsetHi, &hdr.blockTableOffsetHi, 2);
}
if (hdr.headerSize >= 68) {
w(kArchiveSize64, &hdr.archiveSize64, 8);
w(kBetTableOffset, &hdr.betTableOffset, 8);
w(kHetTableOffset, &hdr.hetTableOffset, 8);
}
if (hdr.headerSize >= 208) {
w(kHashTableSize64, &hdr.hashTableSize64, 8);
w(kBlockTableSize64, &hdr.blockTableSize64, 8);
w(kHiBlockTableSize64, &hdr.hiBlockTableSize64, 8);
w(kHetTableSize64, &hdr.hetTableSize64, 8);
w(kBetTableSize64, &hdr.betTableSize64, 8);
w(kRawChunkSize, &hdr.rawChunkSize, 4);
w(kBlockTableMd5, hdr.blockTableMd5.data(), hdr.blockTableMd5.size());
w(kHashTableMd5, hdr.hashTableMd5.data(), hdr.hashTableMd5.size());
w(kHiBlockTableMd5, hdr.hiBlockTableMd5.data(), hdr.hiBlockTableMd5.size());
w(kBetTableMd5, hdr.betTableMd5.data(), hdr.betTableMd5.size());
w(kHetTableMd5, hdr.hetTableMd5.data(), hdr.hetTableMd5.size());
w(kMpqHeaderMd5, hdr.mpqHeaderMd5.data(), hdr.mpqHeaderMd5.size());
}
}
}
std::vector<u8> writeArchive(const MpqHeader& header, const std::vector<WriteEntry>& entries,
u32 hashTableCapacity, interfaces::WorkerPool* pool) {
u32 const htCapacity = nextPowerOf2(hashTableCapacity);
std::vector<u8> archive;
archive.reserve(1024 * 1024);
MpqHeader hdr = header;
hdr.hashTableEntries = htCapacity;
hdr.blockTableEntries = 0;
if (hdr.headerSize < 32) {
hdr.headerSize = 32;
}
size_t const headerSize = hdr.headerSize;
archive.resize(headerSize, 0);
HashTable hashTable;
hashTable.createEmpty(htCapacity);
BlockTable blockTable;
blockTable.createEmpty();
std::vector<std::string> listfileNames;
FileAttributes attrs;
size_t overlayCount = 0;
for (const auto& entry : entries) {
if (entry.rawSectors.empty())
++overlayCount;
}
auto appendEntry = [&](const WriteEntry& entry, EncodedFile* encoded) {
u32 const fileOffset = static_cast<u32>(archive.size());
BlockEntry be;
if (!entry.rawSectors.empty()) {
archive.insert(archive.end(), entry.rawSectors.begin(), entry.rawSectors.end());
be = makeRawCopyBlockEntry(fileOffset, entry.sourceBlock);
} else {
if (encoded->data.empty() && !entry.rawData.empty()) {
encoded->data = entry.rawData;
encoded->compressedSize = static_cast<u32>(entry.rawData.size());
encoded->flags = FileFlag::kExists;
}
archive.insert(archive.end(), encoded->data.begin(), encoded->data.end());
be = makeBlockEntry(fileOffset, *encoded, static_cast<u32>(entry.rawData.size()));
}
u32 const blockIndex = blockTable.append(be);
(void)hashTable.insert(entry.filename, entry.locale, blockIndex);
listfileNames.push_back(entry.filename);
};
std::vector<EncodedFile> encodedFiles(entries.size());
auto makeEncodeOpts = [&](const WriteEntry& entry) {
EncodeOptions opts;
opts.compression = entry.compression;
opts.encrypt = entry.encrypt;
opts.singleUnit = entry.singleUnit;
opts.sectorSize = hdr.sectorSize();
opts.filename = entry.filename;
return opts;
};
if (pool && pool->threadCount() > 0 && overlayCount >= 2) {
std::vector<std::pair<std::span<const u8>, EncodeOptions>> batchItems;
std::vector<size_t> batchToEntry;
batchItems.reserve(overlayCount);
batchToEntry.reserve(overlayCount);
for (size_t idx = 0; idx < entries.size(); ++idx) {
if (!entries[idx].rawSectors.empty())
continue;
batchItems.emplace_back(std::span<const u8>(entries[idx].rawData),
makeEncodeOpts(entries[idx]));
batchToEntry.push_back(idx);
}
auto batchResult = encodeBatch(batchItems, pool);
for (size_t b = 0; b < batchResult.files.size(); ++b) {
encodedFiles[batchToEntry[b]] = std::move(batchResult.files[b]);
}
} else {
for (size_t idx = 0; idx < entries.size(); ++idx) {
const auto& entry = entries[idx];
if (!entry.rawSectors.empty())
continue;
encodedFiles[idx] =
encodeFileData(std::span<const u8>(entry.rawData), makeEncodeOpts(entry), pool);
}
}
for (size_t idx = 0; idx < entries.size(); ++idx) {
appendEntry(entries[idx], &encodedFiles[idx]);
}
auto appendSpecialFile = [&](const std::string& name, std::vector<u8>& rawData) {
u32 const uncompressedSize = static_cast<u32>(rawData.size());
u32 const fileOffset = static_cast<u32>(archive.size());
EncodeOptions opts;
opts.compression = CompressionFlag::kZlib;
opts.sectorSize = hdr.sectorSize();
opts.filename = name;
auto encoded = encodeFileData(std::span<const u8>(rawData), opts);
if (encoded.data.empty() && !rawData.empty()) {
encoded.data = std::move(rawData);
encoded.compressedSize = static_cast<u32>(encoded.data.size());
encoded.flags = FileFlag::kExists;
}
archive.insert(archive.end(), encoded.data.begin(), encoded.data.end());
BlockEntry const be = makeBlockEntry(fileOffset, encoded, uncompressedSize);
u32 const blockIndex = blockTable.append(be);
(void)hashTable.insert(name, 0, blockIndex);
};
listfileNames.push_back("(listfile)");
listfileNames.push_back("(attributes)");
auto listfileData = buildListfile(listfileNames);
appendSpecialFile("(listfile)", listfileData);
attrs.crc32s.resize(blockTable.count(), 0);
attrs.filetimes.resize(blockTable.count(), currentFiletime());
attrs.md5s.resize(blockTable.count(), std::array<u8, 16>{});
auto attrData = buildAttributes(attrs);
appendSpecialFile("(attributes)", attrData);
u32 const hashTableOffset = static_cast<u32>(archive.size());
auto hashTableData = hashTable.serialize();
archive.insert(archive.end(), hashTableData.begin(), hashTableData.end());
u32 const blockTableOffset = static_cast<u32>(archive.size());
auto blockTableData = blockTable.serialize();
archive.insert(archive.end(), blockTableData.begin(), blockTableData.end());
if (hdr.formatVersion >= 1 && blockTable.needsHiBlockTable()) {
auto hiData = blockTable.serializeHiBlockTable();
hdr.hiBlockTableOffset = archive.size();
archive.insert(archive.end(), hiData.begin(), hiData.end());
}
hdr.hashTableOffset = hashTableOffset;
hdr.blockTableOffset = blockTableOffset;
hdr.blockTableEntries = blockTable.count();
hdr.archiveSize = static_cast<u32>(archive.size());
if (hdr.formatVersion >= 2) {
hdr.archiveSize64 = archive.size();
}
serializeHeader(archive, hdr);
return archive;
}
}