Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a398878aaa | ||
|
|
628d8a3c05 |
@ -48,53 +48,32 @@ bool CoalescedFile::load(const QString& path)
|
|||||||
m_isCoalesced = true;
|
m_isCoalesced = true;
|
||||||
m_loadedPath = path;
|
m_loadedPath = path;
|
||||||
|
|
||||||
// Parse the coalesced format
|
QDataStream stream(data);
|
||||||
const char* ptr = data.constData();
|
stream.setByteOrder(QDataStream::BigEndian);
|
||||||
const char* end = ptr + data.size();
|
|
||||||
|
|
||||||
// Read version (big-endian)
|
quint32 entryCount;
|
||||||
if (ptr + 4 > end) {
|
stream >> entryCount;
|
||||||
setError("Unexpected end of file reading header");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_version = qFromBigEndian<quint32>(reinterpret_cast<const uchar*>(ptr));
|
entryCount /= 2;
|
||||||
ptr += 4;
|
|
||||||
|
|
||||||
// Read entries until EOF
|
for (uint i = 0; i < entryCount; i++)
|
||||||
while (ptr + 4 <= end) {
|
{
|
||||||
quint32 filenameLen = qFromBigEndian<quint32>(reinterpret_cast<const uchar*>(ptr));
|
quint32 filenameLen;
|
||||||
ptr += 4;
|
stream >> filenameLen;
|
||||||
|
|
||||||
// Sanity check filename length - 0 or very large means we've hit end of entries
|
|
||||||
if (filenameLen == 0 || filenameLen > 1024) {
|
if (filenameLen == 0 || filenameLen > 1024) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read filename
|
QByteArray filename(filenameLen - 1, Qt::Uninitialized);
|
||||||
if (ptr + filenameLen > end) {
|
stream.readRawData(filename.data(), filenameLen - 1);
|
||||||
break;
|
stream.skipRawData(1);
|
||||||
}
|
|
||||||
|
|
||||||
// Filename includes null terminator
|
quint32 contentLen;
|
||||||
QString filename = QString::fromLatin1(ptr, filenameLen > 0 ? filenameLen - 1 : 0);
|
stream >> contentLen;
|
||||||
ptr += filenameLen;
|
|
||||||
|
|
||||||
// Read content length
|
QByteArray content(contentLen - 1, Qt::Uninitialized);
|
||||||
if (ptr + 4 > end) {
|
stream.readRawData(content.data(), contentLen - 1);
|
||||||
break;
|
stream.skipRawData(1);
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
IniEntry entry;
|
||||||
entry.filename = filename;
|
entry.filename = filename;
|
||||||
@ -116,10 +95,11 @@ bool CoalescedFile::save(const QString& path) const
|
|||||||
stream.setByteOrder(QDataStream::BigEndian);
|
stream.setByteOrder(QDataStream::BigEndian);
|
||||||
|
|
||||||
// Write version
|
// Write version
|
||||||
stream << m_version;
|
stream << (quint32)m_entries.size() * 2;
|
||||||
|
|
||||||
// Write each entry
|
// Write each entry
|
||||||
for (const IniEntry& entry : m_entries) {
|
for (uint i = 0; i < m_entries.size(); i++) {
|
||||||
|
const IniEntry& entry = m_entries.at(i);
|
||||||
// Convert filename to Latin1 with null terminator
|
// Convert filename to Latin1 with null terminator
|
||||||
QByteArray filenameBytes = entry.filename.toLatin1();
|
QByteArray filenameBytes = entry.filename.toLatin1();
|
||||||
filenameBytes.append('\0');
|
filenameBytes.append('\0');
|
||||||
@ -131,7 +111,7 @@ bool CoalescedFile::save(const QString& path) const
|
|||||||
stream.writeRawData(filenameBytes.constData(), filenameBytes.size());
|
stream.writeRawData(filenameBytes.constData(), filenameBytes.size());
|
||||||
|
|
||||||
// Write content length
|
// Write content length
|
||||||
stream << static_cast<quint32>(entry.content.size());
|
stream << static_cast<quint32>(entry.content.size() + 1);
|
||||||
|
|
||||||
// Write content
|
// Write content
|
||||||
stream.writeRawData(entry.content.constData(), entry.content.size());
|
stream.writeRawData(entry.content.constData(), entry.content.size());
|
||||||
@ -237,10 +217,38 @@ bool CoalescedFile::packDirectory(const QString& dirPath, const QString& basePat
|
|||||||
filters << "*.ini" << "*.int" << "*.jpn" << "*.deu" << "*.esn"
|
filters << "*.ini" << "*.int" << "*.jpn" << "*.deu" << "*.esn"
|
||||||
<< "*.fra" << "*.ita" << "*.kor" << "*.pol" << "*.rus"
|
<< "*.fra" << "*.ita" << "*.kor" << "*.pol" << "*.rus"
|
||||||
<< "*.cze" << "*.hun" << "*.esm" << "*.ptb";
|
<< "*.cze" << "*.hun" << "*.esm" << "*.ptb";
|
||||||
QDirIterator it(dirPath, filters, QDir::Files, QDirIterator::Subdirectories);
|
|
||||||
|
|
||||||
while (it.hasNext()) {
|
int entryCount = 0;
|
||||||
QString filePath = it.next();
|
QDirIterator entryIt(dirPath, filters, QDir::Files, QDirIterator::Subdirectories);
|
||||||
|
while (entryIt.hasNext())
|
||||||
|
{
|
||||||
|
entryIt.next();
|
||||||
|
entryCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read temp file for entry records
|
||||||
|
QFile tempFile(dirPath + "/temp");
|
||||||
|
if (!tempFile.open(QIODevice::ReadOnly))
|
||||||
|
{
|
||||||
|
setError(QString("Failed to open temp file: %1").arg(tempFile.errorString()));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse filenames
|
||||||
|
QStringList entryStrings;
|
||||||
|
for (int i = 0; i < entryCount; i++)
|
||||||
|
{
|
||||||
|
QByteArray entryStr(64, Qt::Uninitialized);
|
||||||
|
qint64 lineLen = tempFile.readLine(entryStr.data(), 64);
|
||||||
|
|
||||||
|
entryStrings.push_back(entryStr.mid(0, lineLen - 1));
|
||||||
|
}
|
||||||
|
tempFile.close();
|
||||||
|
|
||||||
|
for (int i = 0; i < entryStrings.size(); i++)
|
||||||
|
{
|
||||||
|
QString entryString = entryStrings.at(i);
|
||||||
|
const QString& filePath = dirPath + entryString.replace("..", "");
|
||||||
QFile file(filePath);
|
QFile file(filePath);
|
||||||
|
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
|||||||
43
main.cpp
43
main.cpp
@ -120,28 +120,34 @@ int cmdInfo(const QStringList& args)
|
|||||||
|
|
||||||
int cmdUnpack(const QStringList& args)
|
int cmdUnpack(const QStringList& args)
|
||||||
{
|
{
|
||||||
if (args.isEmpty()) {
|
if (args.isEmpty())
|
||||||
|
{
|
||||||
printError(QString("Usage: %1 unpack <file> [output_dir]").arg(QCoreApplication::applicationName()));
|
printError(QString("Usage: %1 unpack <file> [output_dir]").arg(QCoreApplication::applicationName()));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
CoalescedFile coalesced;
|
CoalescedFile coalesced;
|
||||||
if (!coalesced.load(args[0])) {
|
if (!coalesced.load(args[0]))
|
||||||
|
{
|
||||||
printError(coalesced.lastError());
|
printError(coalesced.lastError());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString outputDir;
|
QString outputDir;
|
||||||
if (args.size() > 1) {
|
if (args.size() > 1)
|
||||||
|
{
|
||||||
outputDir = args[1];
|
outputDir = args[1];
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// Default: use filename with _ instead of . (e.g., coalesced.ini -> coalesced_ini)
|
// Default: use filename with _ instead of . (e.g., coalesced.ini -> coalesced_ini)
|
||||||
QFileInfo fi(args[0]);
|
QFileInfo fi(args[0]);
|
||||||
outputDir = fi.completeBaseName() + "_" + fi.suffix();
|
outputDir = fi.completeBaseName() + "_" + fi.suffix();
|
||||||
}
|
}
|
||||||
|
|
||||||
QDir dir(outputDir);
|
QDir dir(outputDir);
|
||||||
if (!dir.exists() && !dir.mkpath(".")) {
|
if (!dir.exists() && !dir.mkpath("."))
|
||||||
|
{
|
||||||
printError(QString("Cannot create output directory: %1").arg(outputDir));
|
printError(QString("Cannot create output directory: %1").arg(outputDir));
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@ -149,11 +155,28 @@ int cmdUnpack(const QStringList& args)
|
|||||||
int extracted = 0;
|
int extracted = 0;
|
||||||
qint64 totalBytes = 0;
|
qint64 totalBytes = 0;
|
||||||
|
|
||||||
for (const IniEntry& entry : coalesced.entries()) {
|
QFile tempFile(outputDir + "/temp");
|
||||||
|
if (!tempFile.open(QIODevice::WriteOnly))
|
||||||
|
{
|
||||||
|
printError(QString("Failed to open temp file: %1").arg(tempFile.errorString()));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < coalesced.entries().size(); i++)
|
||||||
|
{
|
||||||
|
QString entryStr(coalesced.entries().at(i).filename + "\n");
|
||||||
|
tempFile.write(entryStr.toUtf8());
|
||||||
|
}
|
||||||
|
tempFile.close();
|
||||||
|
|
||||||
|
for (int i = 0; i < coalesced.entries().size(); i++)
|
||||||
|
{
|
||||||
|
const IniEntry& entry = coalesced.entries().at(i);
|
||||||
QString localPath = entry.filename;
|
QString localPath = entry.filename;
|
||||||
|
|
||||||
// Remove leading ".." components for safety
|
// Remove leading ".." components for safety
|
||||||
while (localPath.startsWith("..\\") || localPath.startsWith("../")) {
|
while (localPath.startsWith("..\\") || localPath.startsWith("../"))
|
||||||
|
{
|
||||||
localPath = localPath.mid(3);
|
localPath = localPath.mid(3);
|
||||||
}
|
}
|
||||||
localPath.replace('\\', '/');
|
localPath.replace('\\', '/');
|
||||||
@ -161,13 +184,15 @@ int cmdUnpack(const QStringList& args)
|
|||||||
QString fullPath = dir.filePath(localPath);
|
QString fullPath = dir.filePath(localPath);
|
||||||
|
|
||||||
QFileInfo fi(fullPath);
|
QFileInfo fi(fullPath);
|
||||||
if (!fi.dir().exists() && !QDir().mkpath(fi.path())) {
|
if (!fi.dir().exists() && !QDir().mkpath(fi.path()))
|
||||||
|
{
|
||||||
printError(QString("Cannot create directory: %1").arg(fi.path()));
|
printError(QString("Cannot create directory: %1").arg(fi.path()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
QFile file(fullPath);
|
QFile file(fullPath);
|
||||||
if (!file.open(QIODevice::WriteOnly)) {
|
if (!file.open(QIODevice::WriteOnly))
|
||||||
|
{
|
||||||
printError(QString("Cannot write file: %1").arg(fullPath));
|
printError(QString("Cannot write file: %1").arg(fullPath));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user