Add main code for v1.0.
This commit is contained in:
parent
c52a4df53d
commit
e5859aa79e
270
coalesced.cpp
Normal file
270
coalesced.cpp
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
66
coalesced.h
Normal file
66
coalesced.h
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#ifndef COALESCED_H
|
||||||
|
#define COALESCED_H
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QList>
|
||||||
|
|
||||||
|
struct IniEntry {
|
||||||
|
QString filename; // Relative path stored in file
|
||||||
|
QByteArray content; // Raw INI content
|
||||||
|
};
|
||||||
|
|
||||||
|
class CoalescedFile {
|
||||||
|
public:
|
||||||
|
CoalescedFile();
|
||||||
|
|
||||||
|
// Load and parse a coalesced file
|
||||||
|
bool load(const QString& path);
|
||||||
|
|
||||||
|
// Write back to coalesced format
|
||||||
|
bool save(const QString& path) const;
|
||||||
|
|
||||||
|
// Get list of contained files
|
||||||
|
QList<IniEntry> entries() const;
|
||||||
|
|
||||||
|
// Get entry by index
|
||||||
|
const IniEntry* entry(int index) const;
|
||||||
|
|
||||||
|
// Get entry by filename
|
||||||
|
const IniEntry* entry(const QString& filename) const;
|
||||||
|
|
||||||
|
// Check if loaded file is coalesced format
|
||||||
|
bool isCoalesced() const;
|
||||||
|
|
||||||
|
// Get number of entries
|
||||||
|
int count() const;
|
||||||
|
|
||||||
|
// Get total content size
|
||||||
|
qint64 totalSize() const;
|
||||||
|
|
||||||
|
// Get the path of the loaded file
|
||||||
|
QString loadedPath() const;
|
||||||
|
|
||||||
|
// Clear loaded data
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
// Get last error message
|
||||||
|
QString lastError() const;
|
||||||
|
|
||||||
|
// Static check if file is coalesced format without full parse
|
||||||
|
static bool isCoalescedFile(const QString& path);
|
||||||
|
|
||||||
|
// Pack a directory of INI files into this object
|
||||||
|
bool packDirectory(const QString& dirPath, const QString& basePath = QString());
|
||||||
|
|
||||||
|
private:
|
||||||
|
QList<IniEntry> m_entries;
|
||||||
|
QString m_loadedPath;
|
||||||
|
QString m_lastError;
|
||||||
|
bool m_isCoalesced;
|
||||||
|
quint32 m_version; // Header value (often 0x1e for UE3)
|
||||||
|
|
||||||
|
void setError(const QString& error);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // COALESCED_H
|
||||||
313
main.cpp
Normal file
313
main.cpp
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
#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 << "UDK Config Extractor v1.0\n";
|
||||||
|
cout << "Extract and pack UDK/UE3 coalesced INI files\n\n";
|
||||||
|
cout << "Usage:\n";
|
||||||
|
cout << " udk-config-extractor <command> [arguments]\n\n";
|
||||||
|
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 ./unpacked\n";
|
||||||
|
cout << " Drag a directory onto exe Packs to ./coalesced.ini\n";
|
||||||
|
cout.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
int cmdList(const QStringList& args)
|
||||||
|
{
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
printError("Usage: udk-config-extractor list <file>");
|
||||||
|
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("Usage: udk-config-extractor info <file>");
|
||||||
|
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("Usage: udk-config-extractor unpack <file> [output_dir]");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoalescedFile coalesced;
|
||||||
|
if (!coalesced.load(args[0])) {
|
||||||
|
printError(coalesced.lastError());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString outputDir = args.size() > 1 ? args[1] : ".";
|
||||||
|
|
||||||
|
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("Usage: udk-config-extractor pack <input_dir> <output_file>");
|
||||||
|
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("Usage: udk-config-extractor extract <file> <index|name> [output]");
|
||||||
|
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-config-extractor");
|
||||||
|
QCoreApplication::setApplicationVersion("1.0");
|
||||||
|
|
||||||
|
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("udk-config-extractor %1\n").arg(QCoreApplication::applicationVersion());
|
||||||
|
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 - pack to coalesced.ini
|
||||||
|
return cmdPack(QStringList() << cmd << "coalesced.ini");
|
||||||
|
} else if (QFile::exists(cmd)) {
|
||||||
|
// File dragged onto exe - unpack to ./unpacked
|
||||||
|
return cmdUnpack(QStringList() << cmd << "unpacked");
|
||||||
|
} else {
|
||||||
|
printError(QString("Unknown command: %1").arg(cmd));
|
||||||
|
showUsage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
udk-config-extractor.pro
Normal file
15
udk-config-extractor.pro
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
QT += core
|
||||||
|
QT -= gui
|
||||||
|
|
||||||
|
TEMPLATE = app
|
||||||
|
CONFIG += console c++17
|
||||||
|
CONFIG -= app_bundle
|
||||||
|
|
||||||
|
TARGET = udk-config-extractor
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
main.cpp \
|
||||||
|
coalesced.cpp
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
coalesced.h
|
||||||
Loading…
x
Reference in New Issue
Block a user