#include "recompiler.h" #include #include #include #include Recompiler::Recompiler(QObject* parent) : QObject(parent) { } bool Recompiler::canRecompile(int journalId) const { const OperationJournal* journal = JournalManager::instance().getJournal(journalId); if (!journal) { return false; } return journal->isExportable(); } QString Recompiler::whyNotRecompilable(int journalId) const { const OperationJournal* journal = JournalManager::instance().getJournal(journalId); if (!journal) { return "Journal not found"; } return journal->whyNotExportable(); } QByteArray Recompiler::recompile(int journalId, const QVariantMap& values) { QStringList errors; QByteArray result = recompileWithErrors(journalId, values, errors); if (!errors.isEmpty()) { throw std::runtime_error(errors.join("; ").toStdString()); } return result; } QByteArray Recompiler::recompileWithErrors(int journalId, const QVariantMap& values, QStringList& outErrors) { outErrors.clear(); m_visitedJournals.clear(); // Clear recursion tracking OperationJournal* journal = JournalManager::instance().getJournal(journalId); if (!journal) { outErrors << "Journal not found"; return QByteArray(); } if (!journal->isExportable()) { outErrors << journal->whyNotExportable(); return QByteArray(); } emit progressChanged(0, "Starting recompilation"); // Create output buffer QByteArray output; QBuffer buffer(&output); buffer.open(QIODevice::WriteOnly); QDataStream out(&buffer); const int totalEntries = journal->entries.size(); int processedEntries = 0; // Walk journal entries in order and write each one for (const JournalEntry& entry : journal->entries) { try { switch (entry.type) { case OpType::ReadScalar: { QVariant value = getValueForField(values, entry); writeScalar(out, entry.scalarType, entry.byteOrder, value); break; } case OpType::ReadBytes: { // For raw bytes, check if we have modified bytes in values QVariant value = getValueForField(values, entry); if (value.typeId() == QMetaType::QByteArray) { writeBytes(out, value.toByteArray()); } else if (!entry.originalBytes.isEmpty()) { writeBytes(out, entry.originalBytes); } else { // Write zeros if we have no data QByteArray zeros(entry.fieldSize, '\0'); writeBytes(out, zeros); } break; } case OpType::ReadCString: { QVariant value = getValueForField(values, entry); QString str = value.toString(); writeCString(out, str); break; } case OpType::ReadWString: { QVariant value = getValueForField(values, entry); QString str = value.toString(); writeWString(out, str, entry.byteOrder); break; } case OpType::Skip: { // Write original padding bytes if available, otherwise zeros writeSkip(out, entry.fieldSize, entry.originalBytes); break; } case OpType::Align: { // Write original alignment padding if available, otherwise zeros writeSkip(out, entry.fieldSize, entry.originalBytes); break; } case OpType::Seek: { // Seek operations are tricky - we need to pad to the target position // The target position is stored in fieldSize qint64 currentPos = buffer.pos(); qint64 targetPos = entry.fieldSize; if (targetPos > currentPos) { // Need to pad to reach target position qint64 padding = targetPos - currentPos; QByteArray zeros(padding, '\0'); writeBytes(out, zeros); } else if (targetPos < currentPos) { // Seeking backwards - this is problematic for linear recompilation // For now, we'll log a warning but continue outErrors << QString("Warning: Backward seek from %1 to %2 in recompilation") .arg(currentPos).arg(targetPos); } break; } case OpType::Computed: { // Computed values don't directly contribute bytes to output // They are derived from other fields break; } case OpType::Decompress: { // Compression write-back: need to recompress nested content // The nested journal contains the decompressed content that may have been modified if (entry.nestedJournalId >= 0) { // Check if compression type is supported if (!isCompressionSupported(entry.compressionType)) { outErrors << QString("Unsupported compression type for write-back: %1") .arg(entry.compressionType); // Fall back to original compressed data if (!entry.originalBytes.isEmpty()) { writeBytes(out, entry.originalBytes); } break; } // Recompile the nested (decompressed) content QByteArray uncompressedData = recompileNested(entry.nestedJournalId, values, entry.fieldName, outErrors); if (uncompressedData.isEmpty()) { // Fallback to original compressed bytes if (!entry.originalBytes.isEmpty()) { writeBytes(out, entry.originalBytes); } break; } // Recompress the data QByteArray compressedData = compress(uncompressedData, entry.compressionType, entry.compressionParams); if (compressedData.isEmpty()) { outErrors << QString("Failed to recompress data with %1") .arg(entry.compressionType); // Fall back to original if (!entry.originalBytes.isEmpty()) { writeBytes(out, entry.originalBytes); } break; } writeBytes(out, compressedData); } else { // No nested journal - just use original bytes if (!entry.originalBytes.isEmpty()) { writeBytes(out, entry.originalBytes); } } break; } case OpType::ParseNested: case OpType::ParseHere: { // Nested parsing - recursive recompilation if (entry.nestedJournalId >= 0) { QByteArray nestedBytes = recompileNested(entry.nestedJournalId, values, entry.fieldName, outErrors); if (!nestedBytes.isEmpty()) { writeBytes(out, nestedBytes); } else { // Fallback to original bytes if recompilation failed OperationJournal* nestedJournal = JournalManager::instance().getJournal(entry.nestedJournalId); if (nestedJournal && !nestedJournal->originalBytes.isEmpty()) { writeBytes(out, nestedJournal->originalBytes); } } } break; } case OpType::RandomAccess: { // Random access entries don't contribute to output // (and shouldn't exist in exportable journals) break; } } } catch (const std::exception& e) { outErrors << QString("Error processing entry '%1': %2") .arg(entry.fieldName).arg(e.what()); } processedEntries++; int percent = (processedEntries * 100) / qMax(1, totalEntries); emit progressChanged(percent, QString("Processing %1").arg(entry.fieldName)); } buffer.close(); emit progressChanged(100, "Recompilation complete"); return output; } QVariant Recompiler::getValueForField(const QVariantMap& values, const JournalEntry& entry) const { // First check if we have a modified value in the values map if (!entry.fieldName.isEmpty() && values.contains(entry.fieldName)) { return values.value(entry.fieldName); } // Fall back to original value from journal return entry.originalValue; } void Recompiler::writeScalar(QDataStream& out, ScalarType type, ByteOrder order, const QVariant& value) { // Set byte order for this write out.setByteOrder(order == ByteOrder::LE ? QDataStream::LittleEndian : QDataStream::BigEndian); switch (type) { case ScalarType::U8: { quint8 v = static_cast(value.toUInt()); out << v; break; } case ScalarType::I8: { qint8 v = static_cast(value.toInt()); out << v; break; } case ScalarType::U16: { quint16 v = static_cast(value.toUInt()); out << v; break; } case ScalarType::I16: { qint16 v = static_cast(value.toInt()); out << v; break; } case ScalarType::U32: { quint32 v = static_cast(value.toUInt()); out << v; break; } case ScalarType::I32: { qint32 v = static_cast(value.toInt()); out << v; break; } case ScalarType::U64: { quint64 v = static_cast(value.toULongLong()); out << v; break; } case ScalarType::I64: { qint64 v = static_cast(value.toLongLong()); out << v; break; } case ScalarType::F32: { float f = value.toFloat(); quint32 bits; memcpy(&bits, &f, 4); out << bits; break; } case ScalarType::F64: { double d = value.toDouble(); quint64 bits; memcpy(&bits, &d, 8); out << bits; break; } case ScalarType::Bool: { quint8 v = value.toBool() ? 1 : 0; out << v; break; } } } void Recompiler::writeBytes(QDataStream& out, const QByteArray& data) { out.writeRawData(data.constData(), data.size()); } void Recompiler::writeCString(QDataStream& out, const QString& str) { QByteArray utf8 = str.toUtf8(); out.writeRawData(utf8.constData(), utf8.size()); // Write null terminator char null = '\0'; out.writeRawData(&null, 1); } void Recompiler::writeWString(QDataStream& out, const QString& str, ByteOrder order) { // Write each character as UTF-16 for (const QChar& ch : str) { quint16 code = ch.unicode(); if (order == ByteOrder::LE) { code = qToLittleEndian(code); } else { code = qToBigEndian(code); } out.writeRawData(reinterpret_cast(&code), 2); } // Write null terminator (2 bytes) quint16 null = 0; out.writeRawData(reinterpret_cast(&null), 2); } void Recompiler::writeSkip(QDataStream& out, qint64 count, const QByteArray& originalBytes) { if (!originalBytes.isEmpty() && originalBytes.size() >= count) { // Use original padding bytes out.writeRawData(originalBytes.constData(), count); } else { // Write zeros QByteArray zeros(count, '\0'); out.writeRawData(zeros.constData(), count); } } QByteArray Recompiler::recompileNested(int nestedJournalId, const QVariantMap& parentValues, const QString& fieldName, QStringList& outErrors) { // Check for circular reference if (m_visitedJournals.contains(nestedJournalId)) { outErrors << QString("Circular reference detected in nested journal %1").arg(nestedJournalId); return QByteArray(); } OperationJournal* nestedJournal = JournalManager::instance().getJournal(nestedJournalId); if (!nestedJournal) { outErrors << QString("Nested journal %1 not found").arg(nestedJournalId); return QByteArray(); } // Check exportability if (!nestedJournal->isExportable()) { // Non-exportable nested structure - return original bytes return nestedJournal->originalBytes; } // Mark as visited m_visitedJournals.insert(nestedJournalId); // Extract values for the nested structure QVariantMap nestedValues = extractNestedValues(parentValues, fieldName, nestedJournal); // Create output buffer for nested structure QByteArray output; QBuffer buffer(&output); buffer.open(QIODevice::WriteOnly); QDataStream out(&buffer); // Walk nested journal entries for (const JournalEntry& entry : nestedJournal->entries) { try { switch (entry.type) { case OpType::ReadScalar: { QVariant value = getValueForField(nestedValues, entry); writeScalar(out, entry.scalarType, entry.byteOrder, value); break; } case OpType::ReadBytes: { QVariant value = getValueForField(nestedValues, entry); if (value.typeId() == QMetaType::QByteArray) { writeBytes(out, value.toByteArray()); } else if (!entry.originalBytes.isEmpty()) { writeBytes(out, entry.originalBytes); } else { QByteArray zeros(entry.fieldSize, '\0'); writeBytes(out, zeros); } break; } case OpType::ReadCString: { QVariant value = getValueForField(nestedValues, entry); writeCString(out, value.toString()); break; } case OpType::ReadWString: { QVariant value = getValueForField(nestedValues, entry); writeWString(out, value.toString(), entry.byteOrder); break; } case OpType::Skip: case OpType::Align: { writeSkip(out, entry.fieldSize, entry.originalBytes); break; } case OpType::Seek: { qint64 currentPos = buffer.pos(); qint64 targetPos = entry.fieldSize; if (targetPos > currentPos) { qint64 padding = targetPos - currentPos; QByteArray zeros(padding, '\0'); writeBytes(out, zeros); } break; } case OpType::Computed: // Computed values don't contribute bytes break; case OpType::Decompress: // Compression handled at outer level break; case OpType::ParseNested: case OpType::ParseHere: { // Recursively recompile deeper nested structures if (entry.nestedJournalId >= 0) { QByteArray deepNestedBytes = recompileNested(entry.nestedJournalId, nestedValues, entry.fieldName, outErrors); if (!deepNestedBytes.isEmpty()) { writeBytes(out, deepNestedBytes); } else { OperationJournal* deepJournal = JournalManager::instance().getJournal(entry.nestedJournalId); if (deepJournal && !deepJournal->originalBytes.isEmpty()) { writeBytes(out, deepJournal->originalBytes); } } } break; } case OpType::RandomAccess: // Should not be in exportable journal break; } } catch (const std::exception& e) { outErrors << QString("Error in nested '%1.%2': %3") .arg(fieldName).arg(entry.fieldName).arg(e.what()); } } buffer.close(); // Remove from visited (allow reuse in other branches) m_visitedJournals.remove(nestedJournalId); return output; } QVariantMap Recompiler::extractNestedValues(const QVariantMap& parentValues, const QString& fieldName, const OperationJournal* nestedJournal) const { QVariantMap nestedValues; // First, try to get nested values from parent values map // Values may be stored as a nested QVariantMap under the field name if (!fieldName.isEmpty() && parentValues.contains(fieldName)) { QVariant fieldValue = parentValues.value(fieldName); if (fieldValue.typeId() == QMetaType::QVariantMap) { nestedValues = fieldValue.toMap(); } } // Also check for dotted keys like "nested.fieldname" QString prefix = fieldName + "."; for (auto it = parentValues.begin(); it != parentValues.end(); ++it) { if (it.key().startsWith(prefix)) { QString subKey = it.key().mid(prefix.length()); nestedValues[subKey] = it.value(); } } // Fill in missing values from journal's original values if (nestedJournal) { for (const JournalEntry& entry : nestedJournal->entries) { if (!entry.fieldName.isEmpty() && !nestedValues.contains(entry.fieldName)) { nestedValues[entry.fieldName] = entry.originalValue; } } } return nestedValues; } bool Recompiler::isCompressionSupported(const QString& compressionType) const { // Supported compression types for write-back static const QSet supported = { "zlib", "zlib_auto", "deflate", "xmem", "oodle" }; return supported.contains(compressionType.toLower()); } QByteArray Recompiler::compress(const QByteArray& data, const QString& compressionType, const QByteArray& params) const { QString type = compressionType.toLower(); if (type == "zlib" || type == "zlib_auto") { // Use settings from params if available if (!params.isEmpty()) { QDataStream paramsStream(params); int level = Z_BEST_COMPRESSION; int windowBits = MAX_WBITS; int memLevel = 8; int strategy = Z_DEFAULT_STRATEGY; // Read parameters if available if (params.size() >= 4) { paramsStream >> level >> windowBits >> memLevel >> strategy; } return Compression::CompressZLIBWithSettings(data, level, windowBits, memLevel, strategy); } return Compression::CompressZLIB(data); } if (type == "deflate") { // Use settings from params if available if (!params.isEmpty()) { QDataStream paramsStream(params); int level = Z_BEST_COMPRESSION; int windowBits = -MAX_WBITS; // Raw deflate int memLevel = 8; int strategy = Z_DEFAULT_STRATEGY; if (params.size() >= 4) { paramsStream >> level >> windowBits >> memLevel >> strategy; } return Compression::CompressDeflateWithSettings(data, level, windowBits, memLevel, strategy); } return Compression::CompressDeflate(data); } if (type == "xmem") { return Compression::CompressXMem(data); } if (type == "oodle") { return Compression::CompressOodle(data); } // Unsupported compression type return QByteArray(); }