271 lines
6.4 KiB
C++
271 lines
6.4 KiB
C++
|
|
#include "coalesced.h"
|
||
|
|
|
||
|
|
#include <QFile>
|
||
|
|
#include <QDir>
|
||
|
|
#include <QDirIterator>
|
||
|
|
#include <QFileInfo>
|
||
|
|
#include <QDataStream>
|
||
|
|
#include <QtEndian>
|
||
|
|
|
||
|
|
CoalescedFile::CoalescedFile()
|
||
|
|
: m_isCoalesced(false)
|
||
|
|
, m_version(0x1e) // Default UE3 version
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
bool CoalescedFile::load(const QString& path)
|
||
|
|
{
|
||
|
|
clear();
|
||
|
|
|
||
|
|
QFile file(path);
|
||
|
|
if (!file.exists()) {
|
||
|
|
setError(QString("File not found: %1").arg(path));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
||
|
|
setError(QString("Cannot open file: %1").arg(file.errorString()));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray data = file.readAll();
|
||
|
|
file.close();
|
||
|
|
|
||
|
|
if (data.size() < 4) {
|
||
|
|
setError("File too small to be a valid coalesced file");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if this is a coalesced file or a normal INI
|
||
|
|
// Normal INI files start with '[' (0x5B)
|
||
|
|
// Coalesced files start with a version (big-endian), first byte typically 0x00
|
||
|
|
if (static_cast<unsigned char>(data[0]) == '[') {
|
||
|
|
setError("This appears to be a normal INI file, not a coalesced file");
|
||
|
|
m_isCoalesced = false;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
m_isCoalesced = true;
|
||
|
|
m_loadedPath = path;
|
||
|
|
|
||
|
|
// Parse the coalesced format
|
||
|
|
const char* ptr = data.constData();
|
||
|
|
const char* end = ptr + data.size();
|
||
|
|
|
||
|
|
// Read version (big-endian)
|
||
|
|
if (ptr + 4 > end) {
|
||
|
|
setError("Unexpected end of file reading header");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
m_version = qFromBigEndian<quint32>(reinterpret_cast<const uchar*>(ptr));
|
||
|
|
ptr += 4;
|
||
|
|
|
||
|
|
// Read entries until EOF
|
||
|
|
while (ptr + 4 <= end) {
|
||
|
|
quint32 filenameLen = qFromBigEndian<quint32>(reinterpret_cast<const uchar*>(ptr));
|
||
|
|
ptr += 4;
|
||
|
|
|
||
|
|
// Sanity check filename length - 0 or very large means we've hit end of entries
|
||
|
|
if (filenameLen == 0 || filenameLen > 1024) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Read filename
|
||
|
|
if (ptr + filenameLen > end) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Filename includes null terminator
|
||
|
|
QString filename = QString::fromLatin1(ptr, filenameLen > 0 ? filenameLen - 1 : 0);
|
||
|
|
ptr += filenameLen;
|
||
|
|
|
||
|
|
// Read content length
|
||
|
|
if (ptr + 4 > end) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
quint32 contentLen = qFromBigEndian<quint32>(reinterpret_cast<const uchar*>(ptr));
|
||
|
|
ptr += 4;
|
||
|
|
|
||
|
|
// Read content
|
||
|
|
if (ptr + contentLen > end) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray content(ptr, contentLen);
|
||
|
|
ptr += contentLen;
|
||
|
|
|
||
|
|
IniEntry entry;
|
||
|
|
entry.filename = filename;
|
||
|
|
entry.content = content;
|
||
|
|
m_entries.append(entry);
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool CoalescedFile::save(const QString& path) const
|
||
|
|
{
|
||
|
|
QFile file(path);
|
||
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
QDataStream stream(&file);
|
||
|
|
stream.setByteOrder(QDataStream::BigEndian);
|
||
|
|
|
||
|
|
// Write version
|
||
|
|
stream << m_version;
|
||
|
|
|
||
|
|
// Write each entry
|
||
|
|
for (const IniEntry& entry : m_entries) {
|
||
|
|
// Convert filename to Latin1 with null terminator
|
||
|
|
QByteArray filenameBytes = entry.filename.toLatin1();
|
||
|
|
filenameBytes.append('\0');
|
||
|
|
|
||
|
|
// Write filename length (including null terminator)
|
||
|
|
stream << static_cast<quint32>(filenameBytes.size());
|
||
|
|
|
||
|
|
// Write filename
|
||
|
|
stream.writeRawData(filenameBytes.constData(), filenameBytes.size());
|
||
|
|
|
||
|
|
// Write content length
|
||
|
|
stream << static_cast<quint32>(entry.content.size());
|
||
|
|
|
||
|
|
// Write content
|
||
|
|
stream.writeRawData(entry.content.constData(), entry.content.size());
|
||
|
|
}
|
||
|
|
|
||
|
|
file.close();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
QList<IniEntry> CoalescedFile::entries() const
|
||
|
|
{
|
||
|
|
return m_entries;
|
||
|
|
}
|
||
|
|
|
||
|
|
const IniEntry* CoalescedFile::entry(int index) const
|
||
|
|
{
|
||
|
|
if (index < 0 || index >= m_entries.count()) {
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
return &m_entries.at(index);
|
||
|
|
}
|
||
|
|
|
||
|
|
const IniEntry* CoalescedFile::entry(const QString& filename) const
|
||
|
|
{
|
||
|
|
for (const IniEntry& e : m_entries) {
|
||
|
|
if (e.filename.compare(filename, Qt::CaseInsensitive) == 0) {
|
||
|
|
return &e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool CoalescedFile::isCoalesced() const
|
||
|
|
{
|
||
|
|
return m_isCoalesced;
|
||
|
|
}
|
||
|
|
|
||
|
|
int CoalescedFile::count() const
|
||
|
|
{
|
||
|
|
return m_entries.count();
|
||
|
|
}
|
||
|
|
|
||
|
|
qint64 CoalescedFile::totalSize() const
|
||
|
|
{
|
||
|
|
qint64 total = 0;
|
||
|
|
for (const IniEntry& entry : m_entries) {
|
||
|
|
total += entry.content.size();
|
||
|
|
}
|
||
|
|
return total;
|
||
|
|
}
|
||
|
|
|
||
|
|
QString CoalescedFile::loadedPath() const
|
||
|
|
{
|
||
|
|
return m_loadedPath;
|
||
|
|
}
|
||
|
|
|
||
|
|
void CoalescedFile::clear()
|
||
|
|
{
|
||
|
|
m_entries.clear();
|
||
|
|
m_loadedPath.clear();
|
||
|
|
m_lastError.clear();
|
||
|
|
m_isCoalesced = false;
|
||
|
|
m_version = 0x1e; // Default UE3 version
|
||
|
|
}
|
||
|
|
|
||
|
|
QString CoalescedFile::lastError() const
|
||
|
|
{
|
||
|
|
return m_lastError;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool CoalescedFile::isCoalescedFile(const QString& path)
|
||
|
|
{
|
||
|
|
QFile file(path);
|
||
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
char firstByte;
|
||
|
|
if (file.read(&firstByte, 1) != 1) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// If first byte is '[', it's a normal INI file
|
||
|
|
// Coalesced files start with a version (big-endian), first byte typically 0x00
|
||
|
|
return firstByte != '[';
|
||
|
|
}
|
||
|
|
|
||
|
|
bool CoalescedFile::packDirectory(const QString& dirPath, const QString& basePath)
|
||
|
|
{
|
||
|
|
clear();
|
||
|
|
|
||
|
|
QDir dir(dirPath);
|
||
|
|
if (!dir.exists()) {
|
||
|
|
setError(QString("Directory not found: %1").arg(dirPath));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
QString effectiveBasePath = basePath.isEmpty() ? dirPath : basePath;
|
||
|
|
|
||
|
|
// Find all .ini files recursively
|
||
|
|
QDirIterator it(dirPath, QStringList() << "*.ini", QDir::Files, QDirIterator::Subdirectories);
|
||
|
|
|
||
|
|
while (it.hasNext()) {
|
||
|
|
QString filePath = it.next();
|
||
|
|
QFile file(filePath);
|
||
|
|
|
||
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
||
|
|
setError(QString("Cannot open file: %1").arg(filePath));
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
IniEntry entry;
|
||
|
|
|
||
|
|
// Calculate relative path
|
||
|
|
QString relativePath = QDir(effectiveBasePath).relativeFilePath(filePath);
|
||
|
|
// Convert to the format used in coalesced files (with ..\)
|
||
|
|
entry.filename = "..\\" + relativePath.replace('/', '\\');
|
||
|
|
entry.content = file.readAll();
|
||
|
|
|
||
|
|
m_entries.append(entry);
|
||
|
|
file.close();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (m_entries.isEmpty()) {
|
||
|
|
setError("No .ini files found in directory");
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
m_isCoalesced = true;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void CoalescedFile::setError(const QString& error)
|
||
|
|
{
|
||
|
|
m_lastError = error;
|
||
|
|
}
|