347 lines
10 KiB
C++
347 lines
10 KiB
C++
#include <QCoreApplication>
|
|
#include <QTextStream>
|
|
#include <QFile>
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
|
|
#include "coalesced.h"
|
|
|
|
static QTextStream cout(stdout);
|
|
static QTextStream cerr(stderr);
|
|
|
|
void printError(const QString& message)
|
|
{
|
|
cerr << "Error: " << message << "\n";
|
|
cerr.flush();
|
|
}
|
|
|
|
void showUsage()
|
|
{
|
|
cout << QString("UDK Config Extractor %1\n").arg(QCoreApplication::applicationVersion());
|
|
cout << "Extract and pack UDK/UE3 coalesced INI/INT files\n\n";
|
|
cout << "Usage:\n";
|
|
cout << QString(" %1 <command> [arguments]\n\n").arg(QCoreApplication::applicationName());
|
|
cout << "Commands:\n";
|
|
cout << " list <file> List files in archive\n";
|
|
cout << " info <file> Show archive metadata\n";
|
|
cout << " unpack <file> [output_dir] Extract all INI files\n";
|
|
cout << " pack <input_dir> <output_file> Pack directory into coalesced file\n";
|
|
cout << " extract <file> <index|name> [output] Extract single file\n\n";
|
|
cout << "Options:\n";
|
|
cout << " -h, --help Show this help message\n";
|
|
cout << " -v, --version Show version\n\n";
|
|
cout << "Drag & Drop:\n";
|
|
cout << " Drag a coalesced file onto exe Extracts to ./<name>_<ext>/\n";
|
|
cout << " Drag a folder onto exe Packs to ./<name>.<ext>\n";
|
|
cout << " (folder coalesced_ini -> coalesced.ini)\n";
|
|
cout.flush();
|
|
}
|
|
|
|
int cmdList(const QStringList& args)
|
|
{
|
|
if (args.isEmpty()) {
|
|
printError(QString("Usage: %1 list <file>").arg(QCoreApplication::applicationName()));
|
|
return 1;
|
|
}
|
|
|
|
CoalescedFile coalesced;
|
|
if (!coalesced.load(args[0])) {
|
|
printError(coalesced.lastError());
|
|
return 1;
|
|
}
|
|
|
|
cout << QString("%1 %2 %3\n")
|
|
.arg("Index", 5)
|
|
.arg("Size", 8)
|
|
.arg("Filename");
|
|
cout << QString("%1 %2 %3\n")
|
|
.arg(QString("-").repeated(5), 5)
|
|
.arg(QString("-").repeated(8), 8)
|
|
.arg(QString("-").repeated(50));
|
|
|
|
const QList<IniEntry>& entries = coalesced.entries();
|
|
for (int i = 0; i < entries.count(); ++i) {
|
|
const IniEntry& entry = entries[i];
|
|
cout << QString("%1 %2 %3\n")
|
|
.arg(i, 5)
|
|
.arg(entry.content.size(), 8)
|
|
.arg(entry.filename);
|
|
}
|
|
|
|
cout << QString("\nTotal: %1 files, %2 bytes\n")
|
|
.arg(coalesced.count())
|
|
.arg(coalesced.totalSize());
|
|
cout.flush();
|
|
return 0;
|
|
}
|
|
|
|
int cmdInfo(const QStringList& args)
|
|
{
|
|
if (args.isEmpty()) {
|
|
printError(QString("Usage: %1 info <file>").arg(QCoreApplication::applicationName()));
|
|
return 1;
|
|
}
|
|
|
|
CoalescedFile coalesced;
|
|
if (!coalesced.load(args[0])) {
|
|
printError(coalesced.lastError());
|
|
return 1;
|
|
}
|
|
|
|
cout << "Archive Information:\n";
|
|
cout << QString(" File: %1\n").arg(coalesced.loadedPath());
|
|
cout << QString(" Entries: %1\n").arg(coalesced.count());
|
|
cout << QString(" Total content size: %1 bytes\n").arg(coalesced.totalSize());
|
|
|
|
qint64 minSize = LLONG_MAX;
|
|
qint64 maxSize = 0;
|
|
QString minFile, maxFile;
|
|
|
|
for (const IniEntry& entry : coalesced.entries()) {
|
|
if (entry.content.size() < minSize) {
|
|
minSize = entry.content.size();
|
|
minFile = entry.filename;
|
|
}
|
|
if (entry.content.size() > maxSize) {
|
|
maxSize = entry.content.size();
|
|
maxFile = entry.filename;
|
|
}
|
|
}
|
|
|
|
if (coalesced.count() > 0) {
|
|
cout << QString(" Smallest: %1 bytes (%2)\n").arg(minSize).arg(minFile);
|
|
cout << QString(" Largest: %1 bytes (%2)\n").arg(maxSize).arg(maxFile);
|
|
cout << QString(" Average: %1 bytes\n").arg(coalesced.totalSize() / coalesced.count());
|
|
}
|
|
|
|
cout.flush();
|
|
return 0;
|
|
}
|
|
|
|
int cmdUnpack(const QStringList& args)
|
|
{
|
|
if (args.isEmpty()) {
|
|
printError(QString("Usage: %1 unpack <file> [output_dir]").arg(QCoreApplication::applicationName()));
|
|
return 1;
|
|
}
|
|
|
|
CoalescedFile coalesced;
|
|
if (!coalesced.load(args[0])) {
|
|
printError(coalesced.lastError());
|
|
return 1;
|
|
}
|
|
|
|
QString outputDir;
|
|
if (args.size() > 1) {
|
|
outputDir = args[1];
|
|
} else {
|
|
// Default: use filename with _ instead of . (e.g., coalesced.ini -> coalesced_ini)
|
|
QFileInfo fi(args[0]);
|
|
outputDir = fi.completeBaseName() + "_" + fi.suffix();
|
|
}
|
|
|
|
QDir dir(outputDir);
|
|
if (!dir.exists() && !dir.mkpath(".")) {
|
|
printError(QString("Cannot create output directory: %1").arg(outputDir));
|
|
return 1;
|
|
}
|
|
|
|
int extracted = 0;
|
|
qint64 totalBytes = 0;
|
|
|
|
for (const IniEntry& entry : coalesced.entries()) {
|
|
QString localPath = entry.filename;
|
|
|
|
// Remove leading ".." components for safety
|
|
while (localPath.startsWith("..\\") || localPath.startsWith("../")) {
|
|
localPath = localPath.mid(3);
|
|
}
|
|
localPath.replace('\\', '/');
|
|
|
|
QString fullPath = dir.filePath(localPath);
|
|
|
|
QFileInfo fi(fullPath);
|
|
if (!fi.dir().exists() && !QDir().mkpath(fi.path())) {
|
|
printError(QString("Cannot create directory: %1").arg(fi.path()));
|
|
continue;
|
|
}
|
|
|
|
QFile file(fullPath);
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
printError(QString("Cannot write file: %1").arg(fullPath));
|
|
continue;
|
|
}
|
|
|
|
file.write(entry.content);
|
|
file.close();
|
|
|
|
extracted++;
|
|
totalBytes += entry.content.size();
|
|
}
|
|
|
|
cout << QString("Extracted %1 files (%2 bytes) to %3\n")
|
|
.arg(extracted)
|
|
.arg(totalBytes)
|
|
.arg(QDir(outputDir).absolutePath());
|
|
cout.flush();
|
|
return 0;
|
|
}
|
|
|
|
int cmdPack(const QStringList& args)
|
|
{
|
|
if (args.size() < 2) {
|
|
printError(QString("Usage: %1 pack <input_dir> <output_file>").arg(QCoreApplication::applicationName()));
|
|
return 1;
|
|
}
|
|
|
|
CoalescedFile packer;
|
|
if (!packer.packDirectory(args[0])) {
|
|
printError(packer.lastError());
|
|
return 1;
|
|
}
|
|
|
|
if (!packer.save(args[1])) {
|
|
printError(QString("Cannot write output file: %1").arg(args[1]));
|
|
return 1;
|
|
}
|
|
|
|
cout << QString("Packed %1 files (%2 bytes) to %3\n")
|
|
.arg(packer.count())
|
|
.arg(packer.totalSize())
|
|
.arg(args[1]);
|
|
cout.flush();
|
|
return 0;
|
|
}
|
|
|
|
int cmdExtract(const QStringList& args)
|
|
{
|
|
if (args.size() < 2) {
|
|
printError(QString("Usage: %1 extract <file> <index|name> [output]").arg(QCoreApplication::applicationName()));
|
|
return 1;
|
|
}
|
|
|
|
CoalescedFile coalesced;
|
|
if (!coalesced.load(args[0])) {
|
|
printError(coalesced.lastError());
|
|
return 1;
|
|
}
|
|
|
|
QString target = args[1];
|
|
const IniEntry* entry = nullptr;
|
|
|
|
bool ok;
|
|
int index = target.toInt(&ok);
|
|
if (ok) {
|
|
entry = coalesced.entry(index);
|
|
if (!entry) {
|
|
printError(QString("Index %1 out of range (0-%2)").arg(index).arg(coalesced.count() - 1));
|
|
return 1;
|
|
}
|
|
} else {
|
|
entry = coalesced.entry(target);
|
|
if (!entry) {
|
|
printError(QString("File not found: %1").arg(target));
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
QString outputPath;
|
|
if (args.size() > 2) {
|
|
outputPath = args[2];
|
|
} else {
|
|
QFileInfo fi(entry->filename);
|
|
outputPath = fi.fileName();
|
|
}
|
|
|
|
QFile file(outputPath);
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
printError(QString("Cannot write file: %1").arg(outputPath));
|
|
return 1;
|
|
}
|
|
|
|
file.write(entry->content);
|
|
file.close();
|
|
|
|
cout << QString("Extracted '%1' to '%2' (%3 bytes)\n")
|
|
.arg(entry->filename)
|
|
.arg(outputPath)
|
|
.arg(entry->content.size());
|
|
cout.flush();
|
|
return 0;
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
QCoreApplication app(argc, argv);
|
|
QCoreApplication::setApplicationName("udk-manip");
|
|
QCoreApplication::setApplicationVersion("1.2");
|
|
|
|
QStringList args = app.arguments();
|
|
args.removeFirst(); // Remove program name
|
|
|
|
if (args.isEmpty()) {
|
|
showUsage();
|
|
return 1;
|
|
}
|
|
|
|
QString cmd = args.takeFirst().toLower();
|
|
|
|
if (cmd == "-h" || cmd == "--help" || cmd == "help") {
|
|
showUsage();
|
|
return 0;
|
|
}
|
|
|
|
if (cmd == "-v" || cmd == "--version" || cmd == "version") {
|
|
cout << QString("%1 %2\n").arg(QCoreApplication::applicationVersion()).arg(QCoreApplication::applicationName());
|
|
cout.flush();
|
|
return 0;
|
|
}
|
|
|
|
if (cmd == "list") {
|
|
return cmdList(args);
|
|
} else if (cmd == "info") {
|
|
return cmdInfo(args);
|
|
} else if (cmd == "unpack") {
|
|
return cmdUnpack(args);
|
|
} else if (cmd == "pack") {
|
|
return cmdPack(args);
|
|
} else if (cmd == "extract") {
|
|
return cmdExtract(args);
|
|
} else if (QFileInfo(cmd).isDir()) {
|
|
// Directory dragged onto exe - derive output filename from folder name
|
|
// Folder "coalesced_ini" -> output "coalesced.ini"
|
|
CoalescedFile packer;
|
|
if (!packer.packDirectory(cmd)) {
|
|
printError(packer.lastError());
|
|
return 1;
|
|
}
|
|
// Get folder name and replace last _ with .
|
|
QString folderName = QFileInfo(cmd).fileName();
|
|
int lastUnderscore = folderName.lastIndexOf('_');
|
|
QString outputFile;
|
|
if (lastUnderscore > 0) {
|
|
outputFile = folderName.left(lastUnderscore) + "." + folderName.mid(lastUnderscore + 1);
|
|
} else {
|
|
// Fallback: use detected extension
|
|
outputFile = folderName + "." + packer.detectExtension();
|
|
}
|
|
if (!packer.save(outputFile)) {
|
|
printError(QString("Cannot write output file: %1").arg(outputFile));
|
|
return 1;
|
|
}
|
|
cout << QString("Packed %1 files (%2 bytes) to %3\n")
|
|
.arg(packer.count())
|
|
.arg(packer.totalSize())
|
|
.arg(outputFile);
|
|
cout.flush();
|
|
return 0;
|
|
} else if (QFile::exists(cmd)) {
|
|
// File dragged onto exe - unpack to folder named after file
|
|
return cmdUnpack(QStringList() << cmd);
|
|
} else {
|
|
printError(QString("Unknown command: %1").arg(cmd));
|
|
showUsage();
|
|
return 1;
|
|
}
|
|
}
|