#include "common/listfile_parser.h"
#include "wow_root.h"
#include "../../common/byte_order.h"
#include "../../common/jenkins.h"
#include <algorithm>
#include <cstring>
namespace whiteout::storages::casc {
using storages::common::readLE32;
using storages::common::readLE64;
using storages::common::readLEi32;
static constexpr size_t kWowBlockHeaderSizeStandard = 12;
static constexpr size_t kWowBlockHeaderSizeV3 = 17;
static constexpr size_t kWowOldRecordSize = 24;
namespace {
struct WowRootHeader {
u32 totalFileCount = 0;
u32 namedFileCount = 0;
u32 headerVersion = 0; size_t dataOffset = 0; };
static bool tryParseHeaderV3(std::span<const u8> data, WowRootHeader& out) {
if (data.size() < 24)
return false;
u32 const magic = readLE32(data.data());
if (magic != RootSignature::kMFST)
return false;
u32 const headerSize = readLE32(data.data() + 4);
u32 const version = readLE32(data.data() + 8);
if (version < 1 || version > 2)
return false;
if (headerSize < 16)
return false;
out.totalFileCount = readLE32(data.data() + 12);
out.namedFileCount = readLE32(data.data() + 16);
out.headerVersion = version + 1; out.dataOffset = headerSize + 4; if (out.dataOffset > data.size())
return false;
return true;
}
static bool tryParseHeaderV2(std::span<const u8> data, WowRootHeader& out) {
if (data.size() < 12)
return false;
u32 const magic = readLE32(data.data());
if (magic != RootSignature::kMFST)
return false;
out.totalFileCount = readLE32(data.data() + 4);
out.namedFileCount = readLE32(data.data() + 8);
out.headerVersion = 1;
out.dataOffset = 12;
return true;
}
static bool tryParseHeaderLegacy(std::span<const u8> data, WowRootHeader& out) {
if (data.size() < 12)
return false;
u32 const numRecords = readLE32(data.data());
if (numRecords == 0 || numRecords > 10000000)
return false;
out.totalFileCount = 0; out.namedFileCount = 0;
out.headerVersion = 0;
out.dataOffset = 0;
return true;
}
struct BlockHeader {
u32 numRecords = 0;
u32 contentFlags = 0;
u32 localeFlags = 0;
size_t headerSize = 0; };
static bool parseBlockHeader(std::span<const u8> data, size_t offset, u32 manifestVersion,
BlockHeader& out) {
if (manifestVersion >= 3) {
if (offset + kWowBlockHeaderSizeV3 > data.size())
return false;
const u8* p = data.data() + offset;
out.numRecords = readLE32(p);
out.localeFlags = readLE32(p + 4);
u32 const unk1 = readLE32(p + 8);
u32 const unk2 = readLE32(p + 12);
u8 const unk3 = p[16];
out.contentFlags = unk1 | unk2 | (u32(unk3) << 17);
out.headerSize = kWowBlockHeaderSizeV3;
} else {
if (offset + kWowBlockHeaderSizeStandard > data.size())
return false;
const u8* p = data.data() + offset;
out.numRecords = readLE32(p);
out.contentFlags = readLE32(p + 4);
out.localeFlags = readLE32(p + 8);
out.headerSize = kWowBlockHeaderSizeStandard;
}
return true;
}
static bool parseBlocks(std::span<const u8> data, const WowRootHeader& header,
std::vector<RootEntry>& outEntries) {
size_t offset = header.dataOffset;
bool const useOldRecordFormat = (header.headerVersion == 0);
bool const allowNonNamedFiles =
(header.totalFileCount != header.namedFileCount) || (header.headerVersion == 0);
while (offset < data.size()) {
BlockHeader bh;
if (!parseBlockHeader(data, offset, header.headerVersion, bh))
break;
offset += bh.headerSize;
if (bh.numRecords == 0)
break;
size_t const deltaSize = size_t(bh.numRecords) * 4;
if (offset + deltaSize > data.size())
return false;
std::vector<u32> fileDataIds(bh.numRecords);
{
i32 fileDataId = 0;
const u8* dp = data.data() + offset;
for (u32 i = 0; i < bh.numRecords; ++i) {
i32 const delta = readLEi32(dp + size_t(i) * 4);
fileDataId += delta;
fileDataIds[i] = u32(fileDataId);
fileDataId++; }
}
offset += deltaSize;
if (useOldRecordFormat) {
size_t const recordSize = size_t(bh.numRecords) * kWowOldRecordSize;
if (offset + recordSize > data.size())
return false;
for (u32 i = 0; i < bh.numRecords; ++i) {
RootEntry entry;
const u8* rp = data.data() + offset + size_t(i) * kWowOldRecordSize;
std::memcpy(entry.cKey.data(), rp, 16);
entry.fileNameHash = readLE64(rp + 16);
entry.fileDataId = fileDataIds[i];
entry.localeFlags = bh.localeFlags;
entry.contentFlags = bh.contentFlags;
outEntries.push_back(std::move(entry));
}
offset += recordSize;
} else {
size_t const cKeySize = size_t(bh.numRecords) * 16;
if (offset + cKeySize > data.size())
return false;
bool const hasNameHash =
!(allowNonNamedFiles && (bh.contentFlags & ContentFlags::NoNameHash));
size_t const nameHashSize = hasNameHash ? (size_t(bh.numRecords) * 8) : 0;
if (offset + cKeySize + nameHashSize > data.size())
return false;
const u8* cKeyBase = data.data() + offset;
const u8* nameBase = hasNameHash ? (data.data() + offset + cKeySize) : nullptr;
for (u32 i = 0; i < bh.numRecords; ++i) {
RootEntry entry;
std::memcpy(entry.cKey.data(), cKeyBase + size_t(i) * 16, 16);
if (nameBase)
entry.fileNameHash = readLE64(nameBase + size_t(i) * 8);
entry.fileDataId = fileDataIds[i];
entry.localeFlags = bh.localeFlags;
entry.contentFlags = bh.contentFlags;
outEntries.push_back(std::move(entry));
}
offset += cKeySize + nameHashSize;
}
}
return !outEntries.empty();
}
}
std::unique_ptr<WowRoot> WowRoot::parse(std::span<const u8> data, interfaces::WorkerPool* pool,
std::span<const u8> listfile) {
if (data.size() < 12)
return nullptr;
WowRootHeader header;
if (!tryParseHeaderV3(data, header) && !tryParseHeaderV2(data, header) &&
!tryParseHeaderLegacy(data, header)) {
return nullptr;
}
auto root = std::make_unique<WowRoot>();
if (!parseBlocks(data, header, root->m_entries))
return nullptr;
root->buildFileDataIdIndex();
if (!listfile.empty()) {
auto pathMap = parseListfile(listfile, pool);
if (!pathMap.empty()) {
root->m_byListfilePath.reserve(pathMap.size());
for (size_t i = 0; i < root->m_entries.size(); ++i) {
auto& entry = root->m_entries[i];
if (entry.fileDataId == kInvalidFileDataId)
continue;
auto it = pathMap.find(entry.fileDataId);
if (it == pathMap.end())
continue;
entry.path = it->second;
auto h = common::jenkinsHash(entry.path);
root->m_byListfilePath.emplace(u64(h.pc) | (u64(h.pb) << 32), i);
}
}
}
return root;
}
std::vector<const RootEntry*> WowRoot::findByPath(const std::string& path) const {
auto hash = common::jenkinsHash(path);
u64 const combined = u64(hash.pc) | (u64(hash.pb) << 32);
if (!m_byListfilePath.empty()) {
auto results = m_byListfilePath.findAll(m_entries, combined);
if (!results.empty())
return results;
}
ensureNameHashIndex();
return m_byNameHash.findAll(m_entries, combined);
}
std::vector<const RootEntry*> WowRoot::findByFileDataId(u32 fileDataId, FileIdHint ) const {
return m_byFileDataId.findAll(m_entries, fileDataId);
}
bool WowRoot::hasFileDataId(u32 fileDataId, FileIdHint ) const {
return m_byFileDataId.contains(fileDataId);
}
std::vector<const RootEntry*> WowRoot::findByCKey(std::span<const u8, 16> cKey) const {
std::vector<const RootEntry*> results;
for (auto& e : m_entries) {
if (e.cKey == std::array<u8, 16>{} ? false
: std::memcmp(e.cKey.data(), cKey.data(), 16) == 0)
results.push_back(&e);
}
return results;
}
void WowRoot::ensureFullyIndexed() const {
ensureNameHashIndex();
}
void WowRoot::buildFileDataIdIndex() {
m_byFileDataId.reserve(m_entries.size());
for (size_t i = 0; i < m_entries.size(); ++i) {
auto& e = m_entries[i];
if (e.fileDataId != kInvalidFileDataId)
m_byFileDataId.emplace(e.fileDataId, i);
}
}
void WowRoot::ensureNameHashIndex() const {
std::call_once(m_nameHashIndexOnce, [this]() {
m_byNameHash.reserve(m_entries.size());
for (size_t i = 0; i < m_entries.size(); ++i) {
auto& e = m_entries[i];
if (e.fileNameHash != 0)
m_byNameHash.emplace(e.fileNameHash, i);
}
});
}
}