XPlor/libs/dsl/operationjournal.h
njohnson 3e3883cfeb Add DSL journaling infrastructure for field editing and write-back
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>
2026-01-12 20:53:20 -05:00

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