Introduce operation journaling to track how values are read from the binary stream, enabling future write-back of edited fields. New components: - OperationJournal: Records read operations with stream offsets, sizes, byte order, and original values during parsing - ReverseEval: Analyzes expressions for invertibility (e.g., x+5 can be reversed to compute the source field when editing the result) - Recompiler: Infrastructure for recompiling modified values back to binary format Interpreter enhancements: - Add runTypeInternalWithJournal() for journaled parsing - Journal scalar reads, byte reads, strings, skip/seek operations - Add deadrising_lzx() built-in for Dead Rising 2 LZX decompression - Support _deadrising_lzx_stem context variable for nested archives Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
246 lines
8.9 KiB
C++
246 lines
8.9 KiB
C++
#ifndef OPERATIONJOURNAL_H
|
|
#define OPERATIONJOURNAL_H
|
|
|
|
#include "ast.h"
|
|
#include <QVector>
|
|
#include <QString>
|
|
#include <QVariant>
|
|
#include <QMap>
|
|
#include <QMutex>
|
|
#include <QSharedPointer>
|
|
|
|
// Types of operations that can be journaled for write-back
|
|
enum class OpType {
|
|
// Primitive reads - directly from stream
|
|
ReadScalar, // u8, u16, u32, u64, i8, i16, i32, i64, f32, f64, bool
|
|
ReadBytes, // read(n)
|
|
ReadCString, // cstring()
|
|
ReadWString, // wstring() or wstring_be()
|
|
|
|
// Position operations - padding/alignment
|
|
Skip, // skip(n) - creates padding bytes
|
|
Align, // align(n) - alignment padding
|
|
Seek, // seek(pos) - absolute position change
|
|
|
|
// Computed values - result of expressions
|
|
Computed, // result of arithmetic/logic (a + b, etc.)
|
|
|
|
// Decompression - marks compression boundary
|
|
Decompress, // zlib, xmem, lzo, deflate, oodle
|
|
|
|
// Nested parsing
|
|
ParseNested, // parse() - creates new stream from bytes
|
|
ParseHere, // parse_here() - delegates to another type at same position
|
|
|
|
// Random access reads - makes file non-exportable
|
|
RandomAccess, // u32at(), bytesat(), i32at(), etc.
|
|
};
|
|
|
|
// Invertible operation type for reverse evaluation
|
|
enum class InvertOp {
|
|
Identity, // x = x
|
|
Add, // x = y + c -> y = x - c
|
|
Sub, // x = y - c -> y = x + c
|
|
SubFrom, // x = c - y -> y = c - x
|
|
Mul, // x = y * c -> y = x / c (if c != 0)
|
|
Div, // x = y / c -> y = x * c
|
|
Shl, // x = y << c -> y = x >> c
|
|
Shr, // x = y >> c -> y = x << c (lossy for non-zero low bits)
|
|
Negate, // x = -y -> y = -x
|
|
|
|
// Non-invertible operations (result in readonly fields)
|
|
BitAnd, // x = y & c -> cannot reverse (information loss)
|
|
BitOr, // x = y | c -> cannot reverse (information loss)
|
|
BitXor, // x = y ^ c -> y = x ^ c (actually invertible!)
|
|
Modulo, // x = y % c -> cannot reverse (information loss)
|
|
};
|
|
|
|
// Chain of operations to invert for reverse evaluation
|
|
struct InverseChain {
|
|
QString sourceField; // The raw field that was read from stream
|
|
QVector<QPair<InvertOp, qint64>> operations; // Operations to apply in reverse
|
|
bool canInvert = true; // false if chain contains non-invertible operation
|
|
QString reason; // Why it can't be inverted (if canInvert is false)
|
|
};
|
|
|
|
// Represents a single journal entry - one parsing operation
|
|
struct JournalEntry {
|
|
OpType type = OpType::ReadScalar;
|
|
QString fieldName; // Variable name this populates (empty for Skip/Align)
|
|
qint64 streamOffset = 0; // Position in stream where this was read
|
|
qint64 fieldSize = 0; // Size in bytes (for reads)
|
|
ByteOrder byteOrder = ByteOrder::LE; // Byte order when read
|
|
ScalarType scalarType = ScalarType::U8; // For ReadScalar
|
|
|
|
// Original bytes that were read (for comparison/restoration)
|
|
QByteArray originalBytes;
|
|
|
|
// For computed values - the expression that produced this
|
|
QString expression; // Original expression string for debugging
|
|
QVector<QString> dependencies; // Field names this depends on
|
|
InverseChain inverseChain; // How to reverse compute source value
|
|
|
|
// For decompression operations
|
|
QString compressionType; // "zlib", "xmem", "lzo", "deflate", "oodle"
|
|
qint64 compressedSize = 0;
|
|
qint64 uncompressedSize = 0;
|
|
QByteArray compressionParams; // Algorithm-specific parameters for recompression
|
|
|
|
// For nested parsing
|
|
QString nestedTypeName; // Type being parsed
|
|
int nestedJournalId = -1; // ID of child journal
|
|
|
|
// UI metadata
|
|
bool hasUiAnnotation = false;
|
|
QString displayName;
|
|
bool isEditable = false; // ui("Name", edit) flag
|
|
|
|
// For validation
|
|
QVariant originalValue; // Value that was parsed (for comparison)
|
|
};
|
|
|
|
// Holds the complete journal for a parsed structure
|
|
struct OperationJournal {
|
|
int id = -1; // Unique journal ID
|
|
QString typeName; // Type that was parsed
|
|
QVector<JournalEntry> entries; // Operations in order
|
|
QByteArray originalBytes; // Complete original bytes for this structure
|
|
|
|
// Hierarchy
|
|
int parentJournalId = -1; // -1 for root
|
|
QVector<int> childJournalIds; // Nested journals
|
|
|
|
// Non-exportable flags
|
|
bool hasRandomAccess = false; // Uses u32at(), bytesat(), etc.
|
|
bool hasScanningLoop = false; // Control flow that scans content
|
|
bool hasUnsupportedCompression = false; // LZO or other unsupported compression
|
|
QString nonExportableReason; // Human-readable reason if not exportable
|
|
|
|
// Check if this journal can be used for write-back
|
|
bool isExportable() const {
|
|
return !hasRandomAccess && !hasScanningLoop && !hasUnsupportedCompression;
|
|
}
|
|
|
|
// Get human-readable reason why not exportable
|
|
QString whyNotExportable() const {
|
|
if (isExportable()) return QString();
|
|
if (hasRandomAccess) return "Uses random access reads (u32at, bytesat, etc.)";
|
|
if (hasScanningLoop) return "Uses content-dependent scanning";
|
|
if (hasUnsupportedCompression) return "Uses unsupported compression: " + nonExportableReason;
|
|
return nonExportableReason;
|
|
}
|
|
};
|
|
|
|
using OperationJournalPtr = QSharedPointer<OperationJournal>;
|
|
|
|
// Singleton manager for all operation journals
|
|
class JournalManager {
|
|
public:
|
|
static JournalManager& instance();
|
|
|
|
// Create new journal for a parse operation
|
|
int createJournal(const QString& typeName, int parentId = -1);
|
|
|
|
// Add entry to journal
|
|
void addEntry(int journalId, const JournalEntry& entry);
|
|
|
|
// Set the field name for the last entry (when we know it after the read)
|
|
void setLastEntryField(int journalId, const QString& fieldName);
|
|
|
|
// Mark journal with non-exportable reason
|
|
void markRandomAccess(int journalId, const QString& functionName = QString());
|
|
void markScanningLoop(int journalId);
|
|
void markUnsupportedCompression(int journalId, const QString& compressionType);
|
|
|
|
// Store original bytes for the journal
|
|
void setOriginalBytes(int journalId, const QByteArray& bytes);
|
|
|
|
// Retrieve journals
|
|
OperationJournal* getJournal(int id);
|
|
const OperationJournal* getJournal(int id) const;
|
|
OperationJournal* getRootJournal(int journalId);
|
|
|
|
// Find journal entry by field name
|
|
JournalEntry* findEntry(int journalId, const QString& fieldName);
|
|
const JournalEntry* findEntry(int journalId, const QString& fieldName) const;
|
|
|
|
// Link child journal to parent
|
|
void linkChildJournal(int parentId, int childId);
|
|
|
|
// Clear all journals (for cleanup between parses)
|
|
void clear();
|
|
|
|
// Get all journals (for debugging)
|
|
QList<int> allJournalIds() const;
|
|
|
|
private:
|
|
JournalManager() = default;
|
|
JournalManager(const JournalManager&) = delete;
|
|
JournalManager& operator=(const JournalManager&) = delete;
|
|
|
|
QMap<int, OperationJournalPtr> m_journals;
|
|
int m_nextId = 0;
|
|
mutable QMutex m_mutex;
|
|
};
|
|
|
|
// Helper to get scalar size
|
|
inline qint64 scalarSize(ScalarType t) {
|
|
switch (t) {
|
|
case ScalarType::U8:
|
|
case ScalarType::I8:
|
|
case ScalarType::Bool:
|
|
return 1;
|
|
case ScalarType::U16:
|
|
case ScalarType::I16:
|
|
return 2;
|
|
case ScalarType::U32:
|
|
case ScalarType::I32:
|
|
case ScalarType::F32:
|
|
return 4;
|
|
case ScalarType::U64:
|
|
case ScalarType::I64:
|
|
case ScalarType::F64:
|
|
return 8;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Helper to convert ScalarType to string
|
|
inline QString scalarTypeName(ScalarType t) {
|
|
switch (t) {
|
|
case ScalarType::U8: return "u8";
|
|
case ScalarType::I8: return "i8";
|
|
case ScalarType::U16: return "u16";
|
|
case ScalarType::I16: return "i16";
|
|
case ScalarType::U32: return "u32";
|
|
case ScalarType::I32: return "i32";
|
|
case ScalarType::U64: return "u64";
|
|
case ScalarType::I64: return "i64";
|
|
case ScalarType::F32: return "f32";
|
|
case ScalarType::F64: return "f64";
|
|
case ScalarType::Bool: return "bool";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
// Helper to convert OpType to string
|
|
inline QString opTypeName(OpType t) {
|
|
switch (t) {
|
|
case OpType::ReadScalar: return "ReadScalar";
|
|
case OpType::ReadBytes: return "ReadBytes";
|
|
case OpType::ReadCString: return "ReadCString";
|
|
case OpType::ReadWString: return "ReadWString";
|
|
case OpType::Skip: return "Skip";
|
|
case OpType::Align: return "Align";
|
|
case OpType::Seek: return "Seek";
|
|
case OpType::Computed: return "Computed";
|
|
case OpType::Decompress: return "Decompress";
|
|
case OpType::ParseNested: return "ParseNested";
|
|
case OpType::ParseHere: return "ParseHere";
|
|
case OpType::RandomAccess: return "RandomAccess";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
#endif // OPERATIONJOURNAL_H
|