// Debug logging through LogManager (controlled by Settings) #include "interpreter.h" #include "dslkeys.h" #include #include "compression.h" #include "utils.h" #include "logmanager.h" #include // Custom exception for break statement struct BreakException : std::exception {}; #include #include #include #include #include Interpreter::Interpreter(Module mod) : m_mod(std::move(mod)) {} void Interpreter::setProgressCallback(ProgressCallback cb) { m_progressCallback = std::move(cb); } void Interpreter::setFileSize(qint64 size) { m_fileSize = size; } void Interpreter::setStatusCallback(StatusCallback cb) { m_statusCallback = std::move(cb); } void Interpreter::reportProgress(Runtime& rt) const { if (m_progressCallback && rt.in && rt.in->device()) { m_progressCallback(rt.in->device()->pos(), m_fileSize); } } void Interpreter::reportStatus(const QString& status) const { if (m_statusCallback) { m_statusCallback(status); } } static QDataStream::ByteOrder toQtOrder(ByteOrder b) { return (b == ByteOrder::LE) ? QDataStream::LittleEndian : QDataStream::BigEndian; } QVariantMap Interpreter::runType(const QString& typeName, QIODevice* dev, const QString &filePath) const { QDataStream ds(dev); return runType(typeName, ds, filePath); } QVariantMap Interpreter::runType(const QString& typeName, QDataStream& stream, const QString& filePath) const { if (!m_mod.types.contains(typeName)) { throw std::runtime_error(("Unknown type: " + typeName).toStdString()); } const qint64 startPos = stream.device() ? stream.device()->pos() : -1; LogManager::instance().debug(QString("[PARSE] Starting type '%1' at pos 0x%2") .arg(typeName) .arg(startPos, 0, 16)); Runtime rt; rt.in = &stream; rt.module = &m_mod; rt.pushType(typeName); // Track root type const TypeDef& td = m_mod.types[typeName]; rt.order = td.order; applyByteOrder(rt); if (!filePath.isEmpty()) { seedFileVars(rt, filePath); } execBlock(rt, td.body); DslKeys::set(rt.vars, DslKey::Type, typeName); const qint64 endPos = stream.device() ? stream.device()->pos() : -1; LogManager::instance().debug(QString("[PARSE] Finished type '%1' at pos 0x%2 (consumed %3 bytes)") .arg(typeName) .arg(endPos, 0, 16) .arg(endPos - startPos)); return rt.vars; } void Interpreter::seedFileVars(Runtime& rt, const QString& filePath) { rt.filePath = filePath; QFileInfo fi(filePath); DslKeys::set(rt.vars, DslKey::Path, filePath); DslKeys::set(rt.vars, DslKey::Name, fi.fileName()); // "mp_test_load.ff" DslKeys::set(rt.vars, DslKey::Basename, fi.completeBaseName()); // "mp_test_load" (without extension) DslKeys::set(rt.vars, DslKey::Ext, fi.suffix().toLower()); // "ff", "xpak", etc. } bool Interpreter::checkCriteria(const QString &typeName, QIODevice *dev, const QString filePath) const { if (!m_mod.types.contains(typeName)) { LogManager::instance().debug(QString("[CRITERIA] Type '%1' not found").arg(typeName)); return false; } const TypeDef& td = m_mod.types[typeName]; if (td.criteria.isEmpty()) { LogManager::instance().debug(QString("[CRITERIA] Type '%1' has no criteria, returning true").arg(typeName)); return true; } LogManager::instance().debug(QString("[CRITERIA] Checking type '%1' for file '%2'").arg(typeName).arg(filePath)); // Save/restore device position const qint64 savedPos = dev->pos(); auto restore = [&]() { dev->seek(savedPos); }; try { QDataStream ds(dev); Runtime rt; rt.in = &ds; rt.module = &m_mod; rt.order = td.order; applyByteOrder(rt); if (!filePath.isEmpty()) { seedFileVars(rt, filePath); } execBlock(rt, td.criteria); restore(); LogManager::instance().debug(QString("[CRITERIA] Type '%1' PASSED criteria").arg(typeName)); return true; } catch (const std::exception& e) { restore(); LogManager::instance().debug(QString("[CRITERIA] Type '%1' FAILED: %2").arg(typeName).arg(e.what())); return false; } catch (...) { restore(); LogManager::instance().debug(QString("[CRITERIA] Type '%1' FAILED (unknown exception)").arg(typeName)); return false; } } QVariantMap Interpreter::runTypeInternal(const QString &typeName, QDataStream &stream, const QString &filePath, std::optional inheritedOrder) const { if (!m_mod.types.contains(typeName)) { throw std::runtime_error(("Unknown type: " + typeName).toStdString()); } Runtime rt; rt.in = &stream; rt.module = &m_mod; rt.pushType(typeName); // Track type for error context const TypeDef& td = m_mod.types[typeName]; // IMPORTANT: inherit if the typedef doesn't specify byteorder if (td.hasExplicitByteOrder) rt.order = td.order; else if (inheritedOrder.has_value()) rt.order = *inheritedOrder; else rt.order = td.order; // root fallback (keeps old behavior) applyByteOrder(rt); if (!filePath.isEmpty()) seedFileVars(rt, filePath); execBlock(rt, td.body); DslKeys::set(rt.vars, DslKey::Type, typeName); return rt.vars; } void Interpreter::applyByteOrder(Runtime& rt) const { if (!rt.in) return; rt.in->setByteOrder(toQtOrder(rt.order)); } qint64 Interpreter::toInt(const QVariant& v) const { if (v.canConvert()) return v.toLongLong(); throw std::runtime_error("Expected integer value"); } bool Interpreter::toBool(const QVariant& v) const { if (v.typeId() == QMetaType::Bool) return v.toBool(); if (v.canConvert()) return v.toLongLong() != 0; if (v.typeId() == QMetaType::QString) return !v.toString().isEmpty(); if (v.canConvert()) return !v.toByteArray().isEmpty(); return v.isValid(); } QVariant Interpreter::readScalar(Runtime& rt, ScalarType t) const { QDataStream& in = *rt.in; QIODevice* dev = in.device(); const qint64 pos = dev ? dev->pos() : -1; auto checkStatus = [&](const char* typeName, int expectedSize) { if (in.status() != QDataStream::Ok) { throw std::runtime_error( QString("Failed to read %1 at pos 0x%2. Parsing: %3") .arg(typeName) .arg(pos, 0, 16) .arg(rt.typeStackString().isEmpty() ? "root" : rt.typeStackString()) .toStdString()); } }; switch (t) { case ScalarType::U8: { quint8 v; in >> v; checkStatus("u8", 1); return v; } case ScalarType::I8: { qint8 v; in >> v; checkStatus("i8", 1); return v; } case ScalarType::U16: { quint16 v; in >> v; checkStatus("u16", 2); return v; } case ScalarType::I16: { qint16 v; in >> v; checkStatus("i16", 2); return v; } case ScalarType::U32: { quint32 v; in >> v; checkStatus("u32", 4); return v; } case ScalarType::I32: { qint32 v; in >> v; checkStatus("i32", 4); return v; } case ScalarType::U64: { quint64 v; in >> v; checkStatus("u64", 8); return v; } case ScalarType::I64: { qint64 v; in >> v; checkStatus("i64", 8); return v; } case ScalarType::F32: { quint32 bits; in >> bits; checkStatus("f32", 4); float v; memcpy(&v, &bits, 4); return v; } case ScalarType::F64: { quint64 bits; in >> bits; checkStatus("f64", 8); double v; memcpy(&v, &bits, 8); return v; } case ScalarType::Bool: { quint8 v; in >> v; checkStatus("bool", 1); return (v != 0); } } return {}; } QByteArray Interpreter::readBytes(Runtime& rt, qint64 n) const { if (n < 0) throw std::runtime_error("read(n): n must be >= 0"); QIODevice* dev = rt.in->device(); if (!dev) throw std::runtime_error("No device in stream"); const qint64 pos = dev->pos(); QByteArray buf; buf.resize(int(n)); const int got = rt.in->readRawData(buf.data(), int(n)); if (got != int(n)) { buf.resize(qMax(0, got)); throw std::runtime_error( QString("Unexpected EOF while reading %1 bytes at pos 0x%2 (got %3). Parsing: %4") .arg(n) .arg(pos, 0, 16) .arg(got) .arg(rt.typeStackString().isEmpty() ? "root" : rt.typeStackString()) .toStdString()); } return buf; } QByteArray Interpreter::readEOF(Runtime& rt) const { QIODevice* dev = rt.in->device(); if (!dev) throw std::runtime_error("No device in stream"); const qint64 remaining = dev->size() - dev->pos(); if (remaining < 0) return {}; return readBytes(rt, remaining); } QVariant Interpreter::evalCall(Runtime& rt, const Expr::Call& c) const { // Builtins: // pos() -> int // size() -> int // read(n) where n is int or EOF -> bytes // zlib(bytes) -> bytes // parse("typeName", bytes) -> map if (c.fn == "pos") { if (!c.args.isEmpty()) throw std::runtime_error("pos() takes no args"); return rt.in->device()->pos(); } if (c.fn == "size") { if (!c.args.isEmpty()) throw std::runtime_error("size() takes no args"); return rt.in->device()->size(); } if (c.fn == "read") { if (c.args.size() != 1) throw std::runtime_error("read(x) takes 1 arg"); QVariant arg = evalExpr(rt, *c.args[0]); // special: EOF token was represented as Var("EOF") if (arg.typeId() == QMetaType::QString && arg.toString() == "EOF") { return readEOF(rt); } return readBytes(rt, toInt(arg)); } if (c.fn == "zlib") { if (c.args.size() != 1) throw std::runtime_error("zlib(bytes) takes 1 arg"); QByteArray in = evalExpr(rt, *c.args[0]).toByteArray(); const QByteArray decompressed = Compression::DecompressZLIB(in); if (!rt.filePath.isEmpty()) { QFileInfo fi(rt.filePath); const QString stem = fi.completeBaseName(); rt.vars["_zlib_stem"] = stem; // Base name for decompressed output } return decompressed; } if (c.fn == "xmem") { if (c.args.size() != 1) throw std::runtime_error("xmem(bytes) takes 1 arg"); QByteArray in = evalExpr(rt, *c.args[0]).toByteArray(); reportStatus(QString("Decompressing %1 bytes...").arg(in.size())); const QByteArray decompressed = Compression::DecompressXMem(in); if (!rt.filePath.isEmpty()) { QFileInfo fi(rt.filePath); const QString stem = fi.completeBaseName(); rt.vars["_xmem_stem"] = stem; // Base name for decompressed output } reportStatus(QString("Decompressed to %1 bytes").arg(decompressed.size())); return decompressed; } if (c.fn == "deflate") { if (c.args.size() != 1) throw std::runtime_error("deflate(bytes) takes 1 arg"); QByteArray in = evalExpr(rt, *c.args[0]).toByteArray(); reportStatus(QString("Decompressing %1 bytes (raw deflate)...").arg(in.size())); const QByteArray decompressed = Compression::DecompressDeflate(in); reportStatus(QString("Decompressed to %1 bytes").arg(decompressed.size())); return decompressed; } if (c.fn == "parse") { if (c.args.size() != 2) throw std::runtime_error("parse(typeName, bytes) takes 2 args"); const QString typeName = evalExpr(rt, *c.args[0]).toString(); const QByteArray bytes = evalExpr(rt, *c.args[1]).toByteArray(); reportStatus(QString("Parsing %1...").arg(typeName)); QDataStream nested(bytes); QVariantMap obj = runTypeInternal(typeName, nested, rt.filePath, rt.order); if (rt.vars.contains("_zlib_stem") && !obj.contains("_zlib_stem")) { obj["_zlib_stem"] = rt.vars["_zlib_stem"]; } // Only set _type if not already set (parse_here delegation sets it) if (!DslKeys::contains(obj, DslKey::Type)) { DslKeys::set(obj, DslKey::Type, typeName); } return obj; } if (c.fn == "parse_here") { if (c.args.size() != 1) throw std::runtime_error("parse_here(typeName) takes 1 arg"); const QString typeName = evalExpr(rt, *c.args[0]).toString(); if (!rt.module->types.contains(typeName)) { throw std::runtime_error(("Unknown type (parse_here): " + typeName).toStdString()); } const qint64 startPos = rt.in->device() ? rt.in->device()->pos() : -1; LogManager::instance().debug(QString("[PARSE_HERE] Parsing '%1' at pos 0x%2") .arg(typeName) .arg(startPos, 0, 16)); // Push type onto stack for error context rt.pushType(typeName); // IMPORTANT: parse using the SAME stream (advance) // We must temporarily apply that type's byteorder. const TypeDef& td = rt.module->types[typeName]; // Save current byteorder const auto oldOrder = rt.order; // Decide child order ByteOrder childOrder = oldOrder; if (td.hasExplicitByteOrder) childOrder = td.order; // Create child runtime sharing stream but inheriting type stack Runtime childRt; childRt.in = rt.in; childRt.module = rt.module; childRt.order = childOrder; childRt.typeStack = rt.typeStack; // Inherit type stack for error context childRt.filePath = rt.filePath; // Inherit file path for naming // Copy file variables to child context if (DslKeys::contains(rt.vars, DslKey::Path)) DslKeys::set(childRt.vars, DslKey::Path, DslKeys::get(rt.vars, DslKey::Path)); if (DslKeys::contains(rt.vars, DslKey::Name)) DslKeys::set(childRt.vars, DslKey::Name, DslKeys::get(rt.vars, DslKey::Name)); if (DslKeys::contains(rt.vars, DslKey::Basename)) DslKeys::set(childRt.vars, DslKey::Basename, DslKeys::get(rt.vars, DslKey::Basename)); if (DslKeys::contains(rt.vars, DslKey::Ext)) DslKeys::set(childRt.vars, DslKey::Ext, DslKeys::get(rt.vars, DslKey::Ext)); applyByteOrder(childRt); execBlock(childRt, td.body); QVariantMap child = childRt.vars; // Pop type from stack rt.popType(); // Restore parent order rt.order = oldOrder; applyByteOrder(rt); // Merge child variables into parent context (for delegation pattern) // Skip reserved metadata variables - parent controls these static const QSet reservedVars = { DslKeys::toString(DslKey::Name), DslKeys::toString(DslKey::Display), DslKeys::toString(DslKey::Type), DslKeys::toString(DslKey::Path), DslKeys::toString(DslKey::Ext), DslKeys::toString(DslKey::Basename) }; for (auto it = child.constBegin(); it != child.constEnd(); ++it) { // Only merge if not reserved, OR if parent doesn't already have it if (!reservedVars.contains(it.key()) || !rt.vars.contains(it.key())) { rt.vars[it.key()] = it.value(); } } // Set _display from child type's display attribute ONLY if parent didn't set it if (!DslKeys::contains(rt.vars, DslKey::Display)) { const QString childDisplay = td.display.isEmpty() ? typeName : td.display; DslKeys::set(rt.vars, DslKey::Display, childDisplay); } // parse_here is delegation - parent BECOMES the child type for UI schema // This overwrites any existing _type because delegation means "I am this type" DslKeys::set(rt.vars, DslKey::Type, typeName); DslKeys::set(child, DslKey::Type, typeName); const qint64 endPos = rt.in->device() ? rt.in->device()->pos() : -1; LogManager::instance().debug(QString("[PARSE_HERE] Finished '%1' at pos 0x%2 (consumed %3 bytes)") .arg(typeName) .arg(endPos, 0, 16) .arg(endPos - startPos)); return child; } if (c.fn == "push") { if (c.args.size() != 2) throw std::runtime_error("push(name, value) takes 2 args"); const QString listName = evalExpr(rt, *c.args[0]).toString(); QVariant v = evalExpr(rt, *c.args[1]); if (v.typeId() != QMetaType::QVariantMap) throw std::runtime_error("push expects a map as 2nd arg"); QVariantList lst = rt.vars.value(listName).toList(); // empty if missing/not a list const int newIndex = lst.size(); lst.push_back(v); rt.vars[listName] = lst; const QVariantMap itemMap = v.toMap(); const QString itemType = DslKeys::getString(itemMap, DslKey::Type); const QString itemName = DslKeys::getString(itemMap, DslKey::Name); LogManager::instance().debug(QString("[PUSH] Added item #%1 to '%2': type='%3', name='%4'") .arg(newIndex) .arg(listName) .arg(itemType) .arg(itemName)); return rt.vars[listName]; } if (c.fn == "get") { if (c.args.size() != 2) throw std::runtime_error("get(container, key) takes 2 args"); QVariant container = evalExpr(rt, *c.args[0]); QVariant key = evalExpr(rt, *c.args[1]); // Array indexing: get(array, index) if (container.typeId() == QMetaType::QVariantList) { const QVariantList list = container.toList(); const qint64 index = toInt(key); if (index < 0 || index >= list.size()) { throw std::runtime_error(QString("get: array index %1 out of bounds (size=%2). Parsing: %3") .arg(index).arg(list.size()) .arg(rt.typeStackString().isEmpty() ? "root" : rt.typeStackString()) .toStdString()); } return list[int(index)]; } // Object field access: get(object, "fieldName") if (container.typeId() == QMetaType::QVariantMap) { const QVariantMap map = container.toMap(); const QString fieldName = key.toString(); if (!map.contains(fieldName)) { throw std::runtime_error(QString("get: field '%1' not found in object. Parsing: %2") .arg(fieldName) .arg(rt.typeStackString().isEmpty() ? "root" : rt.typeStackString()) .toStdString()); } return map.value(fieldName); } throw std::runtime_error("get: first argument must be an array or object"); } if (c.fn == "set") { if (c.args.size() != 3) throw std::runtime_error("set(varName, key, value) takes 3 args"); const QString varName = evalExpr(rt, *c.args[0]).toString(); QVariant key = evalExpr(rt, *c.args[1]); QVariant value = evalExpr(rt, *c.args[2]); if (!rt.vars.contains(varName)) { throw std::runtime_error(QString("set: variable '%1' not found").arg(varName).toStdString()); } QVariant container = rt.vars[varName]; // Array indexing: set("arrayVar", index, value) if (container.typeId() == QMetaType::QVariantList) { QVariantList list = container.toList(); const qint64 index = toInt(key); if (index < 0 || index >= list.size()) { throw std::runtime_error(QString("set: array index %1 out of bounds (size=%2)") .arg(index).arg(list.size()).toStdString()); } list[int(index)] = value; rt.vars[varName] = list; return value; } // Object field access: set("objectVar", "fieldName", value) if (container.typeId() == QMetaType::QVariantMap) { QVariantMap map = container.toMap(); const QString fieldName = key.toString(); map[fieldName] = value; rt.vars[varName] = map; return value; } throw std::runtime_error("set: variable must be an array or object"); } auto readAt = [&](qint64 offset, int nbytes)->QByteArray { QIODevice* dev = rt.in->device(); if (!dev) throw std::runtime_error("No device"); if (offset < 0) throw std::runtime_error("readAt: offset must be >= 0"); if (nbytes < 0) throw std::runtime_error("readAt: nbytes must be >= 0"); const qint64 saved = dev->pos(); if (!dev->seek(offset)) throw std::runtime_error("seek failed"); QByteArray buf(nbytes, Qt::Uninitialized); const int got = rt.in->readRawData(buf.data(), nbytes); dev->seek(saved); if (got != nbytes) throw std::runtime_error("readAt short read"); return buf; }; // u8at(offset) if (c.fn == "u8at") { if (c.args.size() != 1) throw std::runtime_error("u8at(off) takes 1 arg"); qint64 off = toInt(evalExpr(rt, *c.args[0])); QByteArray b = readAt(off, 1); return quint8(b[0]); } if (c.fn == "u16at") { if (c.args.size() != 1) throw std::runtime_error("u16at(off) takes 1 arg"); qint64 off = toInt(evalExpr(rt, *c.args[0])); QByteArray b = readAt(off, 2); const quint8 b0 = quint8(b[0]), b1 = quint8(b[1]); if (rt.order == ByteOrder::LE) return quint16(b0 | (b1 << 8)); return quint16((b0 << 8) | b1); } if (c.fn == "u32at") { if (c.args.size() != 1) throw std::runtime_error("u32at(off) takes 1 arg"); qint64 off = toInt(evalExpr(rt, *c.args[0])); QByteArray b = readAt(off, 4); quint32 v = 0; if (rt.order == ByteOrder::LE) { v = quint32(quint8(b[0])) | (quint32(quint8(b[1])) << 8) | (quint32(quint8(b[2])) << 16) | (quint32(quint8(b[3])) << 24); } else { v = (quint32(quint8(b[0])) << 24) | (quint32(quint8(b[1])) << 16) | (quint32(quint8(b[2])) << 8) | quint32(quint8(b[3])); } return v; } if (c.fn == "i32at") { if (c.args.size() != 1) throw std::runtime_error("i32at(off) takes 1 arg"); qint64 off = toInt(evalExpr(rt, *c.args[0])); QByteArray b = readAt(off, 4); qint32 v = 0; if (rt.order == ByteOrder::LE) { v = qint32(quint8(b[0])) | (qint32(quint8(b[1])) << 8) | (qint32(quint8(b[2])) << 16) | (qint32(quint8(b[3])) << 24); } else { v = (qint32(quint8(b[0])) << 24) | (qint32(quint8(b[1])) << 16) | (qint32(quint8(b[2])) << 8) | qint32(quint8(b[3])); } return v; } if (c.fn == "u64at") { if (c.args.size() != 1) throw std::runtime_error("u64at(off) takes 1 arg"); qint64 off = toInt(evalExpr(rt, *c.args[0])); QByteArray b = readAt(off, 8); quint64 v = 0; if (rt.order == ByteOrder::LE) { for (int i = 0; i < 8; ++i) v |= (quint64(quint8(b[i])) << (8 * i)); } else { for (int i = 0; i < 8; ++i) v = (v << 8) | quint8(b[i]); } return v; } // optional helper for magic checks if (c.fn == "bytesat") { if (c.args.size() != 2) throw std::runtime_error("bytesat(off,len) takes 2 args"); qint64 off = toInt(evalExpr(rt, *c.args[0])); int len = int(toInt(evalExpr(rt, *c.args[1]))); return readAt(off, len); } // Extract bytes from a byte array: bytesof(data, off, len) if (c.fn == "bytesof") { if (c.args.size() != 3) throw std::runtime_error("bytesof(data, off, len) takes 3 args"); QByteArray data = evalExpr(rt, *c.args[0]).toByteArray(); qint64 off = toInt(evalExpr(rt, *c.args[1])); int len = int(toInt(evalExpr(rt, *c.args[2]))); if (off < 0 || off >= data.size()) return QByteArray(); if (off + len > data.size()) len = data.size() - off; return data.mid(off, len); } if (c.fn == "ascii") { if (c.args.size() != 1) throw std::runtime_error("ascii(bytes) takes 1 arg"); const QByteArray b = evalExpr(rt, *c.args[0]).toByteArray(); return QString::fromLatin1(b); // strict 1:1 byte->char (good for magic tags) } if (c.fn == "utf8") { if (c.args.size() != 1) throw std::runtime_error("utf8(bytes) takes 1 arg"); const QByteArray b = evalExpr(rt, *c.args[0]).toByteArray(); return QString::fromUtf8(b); } if (c.fn == "utf16be") { if (c.args.size() != 1) throw std::runtime_error("utf16be(bytes) takes 1 arg"); const QByteArray b = evalExpr(rt, *c.args[0]).toByteArray(); // Convert UTF-16 Big Endian to QString QString result; for (int i = 0; i + 1 < b.size(); i += 2) { quint16 ch = (static_cast(b[i]) << 8) | static_cast(b[i + 1]); if (ch == 0) break; // null terminator result.append(QChar(ch)); } return result; } if (c.fn == "utf16le") { if (c.args.size() != 1) throw std::runtime_error("utf16le(bytes) takes 1 arg"); const QByteArray b = evalExpr(rt, *c.args[0]).toByteArray(); // Convert UTF-16 Little Endian to QString QString result; for (int i = 0; i + 1 < b.size(); i += 2) { quint16 ch = static_cast(b[i]) | (static_cast(b[i + 1]) << 8); if (ch == 0) break; // null terminator result.append(QChar(ch)); } return result; } if (c.fn == "cstring") { if (c.args.size() != 0) throw std::runtime_error("cstring() takes 0 args"); // Read null-terminated string (C-style string) const qint64 startPos = rt.in->device() ? rt.in->device()->pos() : -1; QByteArray result; char ch; while (true) { if (rt.in->readRawData(&ch, 1) != 1) { throw std::runtime_error( QString("cstring(): unexpected EOF at pos 0x%1. Parsing: %2") .arg(rt.in->device() ? rt.in->device()->pos() : -1, 0, 16) .arg(rt.typeStackString().isEmpty() ? "root" : rt.typeStackString()) .toStdString()); } if (ch == '\0') break; result.append(ch); } return QString::fromUtf8(result); } // wstring() - read UTF-16LE null-terminated string if (c.fn == "wstring") { if (c.args.size() != 0) throw std::runtime_error("wstring() takes 0 args"); QString result; while (true) { quint16 ch; if (rt.in->readRawData(reinterpret_cast(&ch), 2) != 2) { throw std::runtime_error( QString("wstring(): unexpected EOF at pos 0x%1. Parsing: %2") .arg(rt.in->device() ? rt.in->device()->pos() : -1, 0, 16) .arg(rt.typeStackString().isEmpty() ? "root" : rt.typeStackString()) .toStdString()); } // Little-endian read ch = qFromLittleEndian(ch); if (ch == 0) break; result.append(QChar(ch)); } return result; } // wstring_be() - read UTF-16BE null-terminated string if (c.fn == "wstring_be") { if (c.args.size() != 0) throw std::runtime_error("wstring_be() takes 0 args"); QString result; while (true) { quint16 ch; if (rt.in->readRawData(reinterpret_cast(&ch), 2) != 2) { throw std::runtime_error( QString("wstring_be(): unexpected EOF at pos 0x%1. Parsing: %2") .arg(rt.in->device() ? rt.in->device()->pos() : -1, 0, 16) .arg(rt.typeStackString().isEmpty() ? "root" : rt.typeStackString()) .toStdString()); } // Big-endian read ch = qFromBigEndian(ch); if (ch == 0) break; result.append(QChar(ch)); } return result; } if (c.fn == "ctx_set") { if (c.args.size() != 2) throw std::runtime_error("ctx_set(name, value) takes 2 args"); const QString name = evalExpr(rt, *c.args[0]).toString(); const QVariant value = evalExpr(rt, *c.args[1]); rt.module->globals[name] = value; return value; } if (c.fn == "ctx_get") { if (c.args.size() != 1) throw std::runtime_error("ctx_get(name) takes 1 arg"); const QString name = evalExpr(rt, *c.args[0]).toString(); if (rt.module->globals.contains(name)) { return rt.module->globals[name]; } return QVariant(0); // Default to 0 if not found } if (c.fn == "set_name") { if (c.args.size() != 1) throw std::runtime_error("set_name(value) takes 1 arg"); const QVariant value = evalExpr(rt, *c.args[0]); DslKeys::set(rt.vars, DslKey::Name, value); return value; } if (c.fn == "set_display") { if (c.args.size() != 1) throw std::runtime_error("set_display(value) takes 1 arg"); const QVariant value = evalExpr(rt, *c.args[0]); DslKeys::set(rt.vars, DslKey::Display, value); return value; } // match(value, case1, result1, case2, result2, ..., default) // Switch/case-like expression: returns the result matching the value if (c.fn == "match") { if (c.args.size() < 3) throw std::runtime_error("match(value, case1, result1, ..., default) takes at least 3 args"); QVariant value = evalExpr(rt, *c.args[0]); // Process pairs: case, result for (int i = 1; i + 1 < c.args.size(); i += 2) { QVariant caseVal = evalExpr(rt, *c.args[i]); // Compare: use same logic as == operator bool matched = false; bool lnumeric = value.canConvert() && (value.typeId() != QMetaType::QString && value.typeId() != QMetaType::QVariantMap); bool rnumeric = caseVal.canConvert() && (caseVal.typeId() != QMetaType::QString && caseVal.typeId() != QMetaType::QVariantMap); if (lnumeric && rnumeric) { matched = toInt(value) == toInt(caseVal); } else { matched = (value == caseVal); } if (matched) { return evalExpr(rt, *c.args[i + 1]); } } // Last arg is the default (if odd number of args after value) if (c.args.size() % 2 == 0) { return evalExpr(rt, *c.args[c.args.size() - 1]); } // No match and no default return QVariant(); } // bit(value, bit_index) - extract single bit (returns 0 or 1) // bit(value, start, count) - extract bit range if (c.fn == "bit") { if (c.args.size() < 2 || c.args.size() > 3) throw std::runtime_error("bit(value, bit) or bit(value, start, count)"); qint64 val = toInt(evalExpr(rt, *c.args[0])); int start = int(toInt(evalExpr(rt, *c.args[1]))); if (c.args.size() == 2) { // Single bit extraction return (val >> start) & 1; } else { // Bit range extraction int count = int(toInt(evalExpr(rt, *c.args[2]))); qint64 mask = (1LL << count) - 1; return (val >> start) & mask; } } // with_seek(offset, expr) - save position, seek to offset, evaluate expr, restore position if (c.fn == "with_seek") { if (c.args.size() != 2) throw std::runtime_error("with_seek(offset, expr) takes 2 args"); qint64 offset = toInt(evalExpr(rt, *c.args[0])); QIODevice* dev = rt.in->device(); if (!dev) throw std::runtime_error("with_seek requires a device"); const qint64 savedPos = dev->pos(); if (!dev->seek(offset)) throw std::runtime_error("with_seek: seek failed"); QVariant result = evalExpr(rt, *c.args[1]); dev->seek(savedPos); return result; } // field(object, fieldName) - get field from object (alias for get with string key) if (c.fn == "field") { if (c.args.size() != 2) throw std::runtime_error("field(object, fieldName) takes 2 args"); QVariant obj = evalExpr(rt, *c.args[0]); QString fieldName = evalExpr(rt, *c.args[1]).toString(); if (obj.typeId() != QMetaType::QVariantMap) { throw std::runtime_error("field: first argument must be an object"); } QVariantMap map = obj.toMap(); if (!map.contains(fieldName)) { return QVariant(); // Return null for missing fields } return map.value(fieldName); } // make_object() - create an empty object/map if (c.fn == "make_object") { if (c.args.size() != 0) throw std::runtime_error("make_object() takes no args"); return QVariantMap(); } // make_list() - create an empty list if (c.fn == "make_list") { if (c.args.size() != 0) throw std::runtime_error("make_list() takes no args"); return QVariantList(); } // append(listName, value) - append value to list variable if (c.fn == "append") { if (c.args.size() != 2) throw std::runtime_error("append(listName, value) takes 2 args"); const QString listName = evalExpr(rt, *c.args[0]).toString(); QVariant val = evalExpr(rt, *c.args[1]); QVariantList lst = rt.vars.value(listName).toList(); lst.append(val); rt.vars[listName] = lst; return lst; } // UI metadata functions - store metadata for UI rendering // ui(varName, displayName) - marks variable for readonly UI display if (c.fn == "ui") { if (c.args.size() < 1 || c.args.size() > 2) throw std::runtime_error("ui(varName[, displayName]) takes 1-2 args"); const QString varName = evalExpr(rt, *c.args[0]).toString(); QString displayName = varName; if (c.args.size() == 2) { displayName = evalExpr(rt, *c.args[1]).toString(); } // Store UI metadata QVariantMap uiMeta = rt.vars.value("_ui_meta").toMap(); QVariantMap fieldMeta; fieldMeta["visible"] = true; fieldMeta["readonly"] = true; fieldMeta["display"] = displayName; uiMeta[varName] = fieldMeta; rt.vars["_ui_meta"] = uiMeta; return true; } // ui_edit(varName, displayName) - marks variable for editable UI display if (c.fn == "ui_edit") { if (c.args.size() < 1 || c.args.size() > 2) throw std::runtime_error("ui_edit(varName[, displayName]) takes 1-2 args"); const QString varName = evalExpr(rt, *c.args[0]).toString(); QString displayName = varName; if (c.args.size() == 2) { displayName = evalExpr(rt, *c.args[1]).toString(); } QVariantMap uiMeta = rt.vars.value("_ui_meta").toMap(); QVariantMap fieldMeta; fieldMeta["visible"] = true; fieldMeta["readonly"] = false; fieldMeta["display"] = displayName; uiMeta[varName] = fieldMeta; rt.vars["_ui_meta"] = uiMeta; return true; } // ui_table(varName, tableName, columns) - marks variable as table display if (c.fn == "ui_table") { if (c.args.size() != 3) throw std::runtime_error("ui_table(varName, tableName, columns) takes 3 args"); const QString varName = evalExpr(rt, *c.args[0]).toString(); const QString tableName = evalExpr(rt, *c.args[1]).toString(); const QString columns = evalExpr(rt, *c.args[2]).toString(); QVariantMap uiMeta = rt.vars.value("_ui_meta").toMap(); QVariantMap fieldMeta; fieldMeta["visible"] = true; fieldMeta["readonly"] = true; fieldMeta["table"] = tableName; fieldMeta["columns"] = columns; uiMeta[varName] = fieldMeta; rt.vars["_ui_meta"] = uiMeta; return true; } // hide(varName) - marks variable as hidden from UI if (c.fn == "hide") { if (c.args.size() != 1) throw std::runtime_error("hide(varName) takes 1 arg"); const QString varName = evalExpr(rt, *c.args[0]).toString(); QVariantMap uiMeta = rt.vars.value("_ui_meta").toMap(); QVariantMap fieldMeta = uiMeta.value(varName).toMap(); fieldMeta["visible"] = false; fieldMeta["hidden"] = true; uiMeta[varName] = fieldMeta; rt.vars["_ui_meta"] = uiMeta; return true; } // set_viewer(viewerType) - sets the viewer type for this object // Valid types: "hex", "text", "image", "audio", "list", "table" if (c.fn == "set_viewer") { if (c.args.size() != 1) throw std::runtime_error("set_viewer(viewerType) takes 1 arg"); const QString viewerType = evalExpr(rt, *c.args[0]).toString().toLower(); // Validate viewer type QStringList validTypes = {"hex", "text", "image", "audio", "list", "table", "raw"}; if (!validTypes.contains(viewerType)) { throw std::runtime_error(("Invalid viewer type: " + viewerType + ". Valid types: hex, text, image, audio, list, table, raw").toStdString()); } DslKeys::set(rt.vars, DslKey::Viewer, viewerType); return viewerType; } // set_text(content) - sets text content for text viewer display if (c.fn == "set_text") { if (c.args.size() != 1) throw std::runtime_error("set_text(content) takes 1 arg"); const QVariant content = evalExpr(rt, *c.args[0]); rt.vars["_text"] = content; return content; } // set_hidden() - marks current object as hidden from tree view if (c.fn == "set_hidden") { if (c.args.size() != 0) throw std::runtime_error("set_hidden() takes no args"); DslKeys::set(rt.vars, DslKey::Hidden, true); return true; } // skip_tree(varName) - prevents a list variable from showing as tree children if (c.fn == "skip_tree") { if (c.args.size() != 1) throw std::runtime_error("skip_tree(varName) takes 1 arg"); const QString varName = evalExpr(rt, *c.args[0]).toString(); DslKeys::setSkipTree(rt.vars, varName); return true; } // set_preview(filename, data) - sets preview data for the current object if (c.fn == "set_preview") { if (c.args.size() != 2) throw std::runtime_error("set_preview(filename, data) takes 2 args"); const QString filename = evalExpr(rt, *c.args[0]).toString(); const QByteArray data = evalExpr(rt, *c.args[1]).toByteArray(); QVariantMap preview; preview["filename"] = filename; preview["data"] = data; DslKeys::set(rt.vars, DslKey::Preview, preview); return preview; } if (c.fn == "write_file") { if (c.args.size() != 2) throw std::runtime_error("write_file(filename, data) takes 2 args"); const QString filename = evalExpr(rt, *c.args[0]).toString(); const QByteArray data = evalExpr(rt, *c.args[1]).toByteArray(); reportStatus(QString("Exporting %1...").arg(filename)); // Use Utils::ExportData which writes to exports/ directory if (!Utils::ExportData(filename, data)) { throw std::runtime_error(("Failed to export file: " + filename).toStdString()); } LogManager::instance().debug(QString("[EXPORT] Exported %1 bytes to exports/%2") .arg(data.size()) .arg(filename)); return data.size(); // Return number of bytes written } if (c.fn == "run_script") { // run_script(script_name, input_data) - runs a Python script and returns output if (c.args.size() < 1 || c.args.size() > 2) throw std::runtime_error("run_script(script_name[, input_data]) takes 1-2 args"); const QString scriptName = evalExpr(rt, *c.args[0]).toString(); QByteArray inputData; if (c.args.size() == 2) { inputData = evalExpr(rt, *c.args[1]).toByteArray(); } // Find scripts directory QString scriptsDir = QCoreApplication::applicationDirPath() + "/scripts"; QString scriptPath = scriptsDir + "/" + scriptName; if (!QFileInfo::exists(scriptPath)) { // Try source directory scriptsDir = QCoreApplication::applicationDirPath() + "/../../../scripts"; scriptPath = scriptsDir + "/" + scriptName; } if (!QFileInfo::exists(scriptPath)) { throw std::runtime_error(("Script not found: " + scriptName).toStdString()); } // Find Python QString python = "python"; QStringList pythonPaths = { "python", "python3", "C:/Python312/python.exe", "C:/Python311/python.exe", "C:/Python310/python.exe", "/usr/bin/python3" }; for (const QString& p : pythonPaths) { if (QFileInfo::exists(p) || p == "python" || p == "python3") { python = p; break; } } reportStatus(QString("Running %1...").arg(scriptName)); QProcess proc; proc.setProgram(python); proc.setArguments({scriptPath, "-"}); // "-" means read from stdin proc.start(); if (!proc.waitForStarted(5000)) { throw std::runtime_error("Failed to start Python process"); } // Write input data if (!inputData.isEmpty()) { proc.write(inputData); } proc.closeWriteChannel(); // Wait for completion (30 second timeout) if (!proc.waitForFinished(30000)) { proc.kill(); throw std::runtime_error("Script timed out"); } if (proc.exitCode() != 0) { QString err = QString::fromUtf8(proc.readAllStandardError()); throw std::runtime_error(("Script error: " + err).toStdString()); } QByteArray output = proc.readAllStandardOutput(); LogManager::instance().debug(QString("[SCRIPT] %1 returned %2 bytes") .arg(scriptName).arg(output.size())); return output; } if (c.fn == "strip") { if (c.args.size() != 2) throw std::runtime_error("strip(string, chars) takes 2 args"); QString str = evalExpr(rt, *c.args[0]).toString(); QString chars = evalExpr(rt, *c.args[1]).toString(); for (const QChar& ch : chars) { str.remove(ch); } return str; } if (c.fn == "basename") { if (c.args.size() != 1) throw std::runtime_error("basename(path) takes 1 arg"); QString path = evalExpr(rt, *c.args[0]).toString(); // Handle both forward and back slashes int lastSlash = path.lastIndexOf('/'); int lastBackslash = path.lastIndexOf('\\'); int pos = qMax(lastSlash, lastBackslash); if (pos >= 0) { return path.mid(pos + 1); } return path; } if (c.fn == "ends_with") { if (c.args.size() != 2) throw std::runtime_error("ends_with(string, suffix) takes 2 args"); QString str = evalExpr(rt, *c.args[0]).toString(); QString suffix = evalExpr(rt, *c.args[1]).toString(); return str.endsWith(suffix, Qt::CaseInsensitive); } if (c.fn == "starts_with") { if (c.args.size() != 2) throw std::runtime_error("starts_with(string, prefix) takes 2 args"); QString str = evalExpr(rt, *c.args[0]).toString(); QString prefix = evalExpr(rt, *c.args[1]).toString(); return str.startsWith(prefix, Qt::CaseInsensitive); } if (c.fn == "to_lower") { if (c.args.size() != 1) throw std::runtime_error("to_lower(string) takes 1 arg"); return evalExpr(rt, *c.args[0]).toString().toLower(); } if (c.fn == "trim") { if (c.args.size() != 1) throw std::runtime_error("trim(string) takes 1 arg"); return evalExpr(rt, *c.args[0]).toString().trimmed(); } if (c.fn == "contains") { if (c.args.size() != 2) throw std::runtime_error("contains(string, substring) takes 2 args"); const QString str = evalExpr(rt, *c.args[0]).toString(); const QString sub = evalExpr(rt, *c.args[1]).toString(); return str.contains(sub) ? 1 : 0; } if (c.fn == "replace") { if (c.args.size() != 3) throw std::runtime_error("replace(string, old, new) takes 3 args"); QString str = evalExpr(rt, *c.args[0]).toString(); const QString oldStr = evalExpr(rt, *c.args[1]).toString(); const QString newStr = evalExpr(rt, *c.args[2]).toString(); return str.replace(oldStr, newStr); } if (c.fn == "replace_xbox_buttons") { if (c.args.size() != 1) throw std::runtime_error("replace_xbox_buttons(string) takes 1 arg"); QString str = evalExpr(rt, *c.args[0]).toString(); // Replace Cyrillic characters used as Xbox button placeholders str.replace(QChar(0x04B2), QString::fromUtf8("[A]")); // A button str.replace(QChar(0x04B3), QString::fromUtf8("[B]")); // B button str.replace(QChar(0x04B4), QString::fromUtf8("[X]")); // X button str.replace(QChar(0x04B5), QString::fromUtf8("[Y]")); // Y button str.replace(QChar(0x04B0), QString::fromUtf8("[Start]")); // Start button str.replace(QChar(0x04B1), QString::fromUtf8("[LB]")); // LB bumper return str; } if (c.fn == "to_float") { if (c.args.size() != 1) throw std::runtime_error("to_float(value) takes 1 arg"); const QString str = evalExpr(rt, *c.args[0]).toString(); bool ok = false; double val = str.toDouble(&ok); return ok ? val : 0.0; } if (c.fn == "format_float") { if (c.args.size() < 1 || c.args.size() > 2) throw std::runtime_error("format_float(value, [decimals]) takes 1-2 args"); double val = evalExpr(rt, *c.args[0]).toDouble(); int decimals = (c.args.size() == 2) ? evalExpr(rt, *c.args[1]).toInt() : 3; return QString::number(val, 'f', decimals); } if (c.fn == "find") { if (c.args.size() != 2) throw std::runtime_error("find(string, substring) takes 2 args"); const QString str = evalExpr(rt, *c.args[0]).toString(); const QString sub = evalExpr(rt, *c.args[1]).toString(); return str.indexOf(sub); } if (c.fn == "substr") { if (c.args.size() != 3) throw std::runtime_error("substr(string, start, length) takes 3 args"); const QString str = evalExpr(rt, *c.args[0]).toString(); int start = evalExpr(rt, *c.args[1]).toInt(); int length = evalExpr(rt, *c.args[2]).toInt(); return str.mid(start, length); } if (c.fn == "split_lines") { if (c.args.size() != 1) throw std::runtime_error("split_lines(string) takes 1 arg"); const QString str = evalExpr(rt, *c.args[0]).toString(); QVariantList result; QString normalized = str; normalized.replace("\r\n", "\n").replace("\r", "\n"); const QStringList lines = normalized.split('\n', Qt::SkipEmptyParts); for (const QString& line : lines) { result.append(line); } return result; } if (c.fn == "split") { if (c.args.size() != 2) throw std::runtime_error("split(string, delimiter) takes 2 args"); const QString str = evalExpr(rt, *c.args[0]).toString(); const QString delim = evalExpr(rt, *c.args[1]).toString(); QVariantList result; const QStringList parts = str.split(delim); for (const QString& part : parts) { result.append(part); } return result; } if (c.fn == "clean") { // Find the last contiguous sequence of printable ASCII characters if (c.args.size() != 1) throw std::runtime_error("clean(string) takes 1 arg"); QString input = evalExpr(rt, *c.args[0]).toString(); QString lastValid; QString current; for (const QChar& ch : input) { ushort code = ch.unicode(); // Printable ASCII (32-126) if (code >= 32 && code <= 126) { current.append(ch); } else { // Hit invalid char - if we had a valid sequence, save it if (!current.trimmed().isEmpty()) { lastValid = current; } current.clear(); } } // Check final segment if (!current.trimmed().isEmpty()) { lastValid = current; } return lastValid.trimmed(); } if (c.fn == "check_criteria") { // check_criteria("type_name") - returns true if that type's criteria match current data if (c.args.size() != 1) throw std::runtime_error("check_criteria(typeName) takes 1 arg"); const QString typeName = evalExpr(rt, *c.args[0]).toString(); if (!rt.module->types.contains(typeName)) { throw std::runtime_error(("Unknown type (check_criteria): " + typeName).toStdString()); } const TypeDef& td = rt.module->types[typeName]; if (td.criteria.isEmpty()) { // No criteria = always matches return true; } // Save current stream position QIODevice* dev = rt.in->device(); if (!dev) { throw std::runtime_error("check_criteria requires a seekable device"); } const qint64 savedPos = dev->pos(); try { // Create a temporary runtime to evaluate the criteria Runtime tempRt; tempRt.in = rt.in; tempRt.module = rt.module; tempRt.order = td.hasExplicitByteOrder ? td.order : rt.order; tempRt.filePath = rt.filePath; // Copy file variables if (DslKeys::contains(rt.vars, DslKey::Path)) DslKeys::set(tempRt.vars, DslKey::Path, DslKeys::get(rt.vars, DslKey::Path)); if (DslKeys::contains(rt.vars, DslKey::Name)) DslKeys::set(tempRt.vars, DslKey::Name, DslKeys::get(rt.vars, DslKey::Name)); if (rt.vars.contains("_basename")) tempRt.vars["_basename"] = rt.vars["_basename"]; if (rt.vars.contains("_ext")) tempRt.vars["_ext"] = rt.vars["_ext"]; applyByteOrder(tempRt); // Execute criteria block execBlock(tempRt, td.criteria); // Restore position dev->seek(savedPos); LogManager::instance().debug(QString("[CHECK_CRITERIA] Type '%1' PASSED").arg(typeName)); return true; } catch (const std::exception& e) { // Restore position on failure dev->seek(savedPos); LogManager::instance().debug(QString("[CHECK_CRITERIA] Type '%1' FAILED: %2").arg(typeName).arg(e.what())); return false; } } if (c.fn == "preview") { // preview(data) - stores image data for UI preview // preview(filename, data) - stores with filename hint if (c.args.size() < 1 || c.args.size() > 2) throw std::runtime_error("preview(data) or preview(filename, data)"); QString filename; QByteArray data; if (c.args.size() == 1) { data = evalExpr(rt, *c.args[0]).toByteArray(); filename = "preview"; } else { filename = evalExpr(rt, *c.args[0]).toString(); data = evalExpr(rt, *c.args[1]).toByteArray(); } // Store preview data in runtime vars for UI to pick up QVariantMap preview; preview["filename"] = filename; preview["data"] = data; preview["size"] = data.size(); DslKeys::set(rt.vars, DslKey::Preview, preview); LogManager::instance().debug(QString("[PREVIEW] Set preview: %1 (%2 bytes)") .arg(filename) .arg(data.size())); return true; } // make_preview(filename, data) - returns a preview map that can be assigned to _preview if (c.fn == "make_preview") { if (c.args.size() != 2) throw std::runtime_error("make_preview(filename, data) takes 2 args"); QString filename = evalExpr(rt, *c.args[0]).toString(); QByteArray data = evalExpr(rt, *c.args[1]).toByteArray(); QVariantMap preview; preview["filename"] = filename; preview["data"] = data; preview["size"] = data.size(); return preview; } // set_preview(varName, filename, data) - sets preview on a specific variable's map if (c.fn == "set_preview") { if (c.args.size() != 3) throw std::runtime_error("set_preview(varName, filename, data) takes 3 args"); QString varName = evalExpr(rt, *c.args[0]).toString(); QString filename = evalExpr(rt, *c.args[1]).toString(); QByteArray data = evalExpr(rt, *c.args[2]).toByteArray(); if (!rt.vars.contains(varName)) { throw std::runtime_error(("set_preview: unknown variable: " + varName).toStdString()); } QVariantMap obj = rt.vars[varName].toMap(); QVariantMap preview; preview["filename"] = filename; preview["data"] = data; preview["size"] = data.size(); DslKeys::set(obj, DslKey::Preview, preview); rt.vars[varName] = obj; LogManager::instance().debug(QString("[SET_PREVIEW] Set preview on %1: %2 (%3 bytes)") .arg(varName) .arg(filename) .arg(data.size())); return true; } if (c.fn == "null_to_lines") { // null_to_lines(data) - converts null-terminated strings to newline-separated text if (c.args.size() != 1) throw std::runtime_error("null_to_lines(data) takes 1 arg"); QByteArray data = evalExpr(rt, *c.args[0]).toByteArray(); // Replace null bytes with newlines data.replace('\0', '\n'); return data; } if (c.fn == "len") { if (c.args.size() != 1) throw std::runtime_error("len(value) takes 1 arg"); QVariant v = evalExpr(rt, *c.args[0]); if (v.typeId() == QMetaType::QString) return v.toString().length(); if (v.typeId() == QMetaType::QByteArray) return v.toByteArray().size(); if (v.typeId() == QMetaType::QVariantList) return v.toList().size(); throw std::runtime_error("len() requires string, bytes, or array"); } // list_at(list, index) - get item from list by index if (c.fn == "list_at") { if (c.args.size() != 2) throw std::runtime_error("list_at(list, index) takes 2 args"); QVariantList list = evalExpr(rt, *c.args[0]).toList(); int index = evalExpr(rt, *c.args[1]).toInt(); if (index < 0 || index >= list.size()) { return QString("?%1").arg(index); } return list.at(index); } // hex(value) - format number as hex string if (c.fn == "hex") { if (c.args.size() != 1) throw std::runtime_error("hex(value) takes 1 arg"); qint64 val = evalExpr(rt, *c.args[0]).toLongLong(); return QString("0x%1").arg(val, 0, 16, QChar('0')).toUpper(); } // u32be(bytes, offset) - read u32 big-endian from byte array if (c.fn == "u32be") { if (c.args.size() != 2) throw std::runtime_error("u32be(bytes, offset) takes 2 args"); QByteArray data = evalExpr(rt, *c.args[0]).toByteArray(); int offset = evalExpr(rt, *c.args[1]).toInt(); if (offset < 0 || offset + 4 > data.size()) { return 0; } quint32 val = ((quint8)data[offset] << 24) | ((quint8)data[offset+1] << 16) | ((quint8)data[offset+2] << 8) | ((quint8)data[offset+3]); return (qint64)val; } // split_nulls(bytes) - split null-terminated strings into list if (c.fn == "split_nulls") { if (c.args.size() != 1) throw std::runtime_error("split_nulls(bytes) takes 1 arg"); QByteArray data = evalExpr(rt, *c.args[0]).toByteArray(); QVariantList result; QByteArray current; for (int i = 0; i < data.size(); i++) { if (data[i] == '\0') { if (!current.isEmpty()) { result.append(QString::fromLatin1(current)); } current.clear(); } else { current.append(data[i]); } } if (!current.isEmpty()) { result.append(QString::fromLatin1(current)); } return result; } // opcode_name(opcode) - get GML opcode name if (c.fn == "opcode_name") { if (c.args.size() != 1) throw std::runtime_error("opcode_name(opcode) takes 1 arg"); int op = evalExpr(rt, *c.args[0]).toInt(); static const QMap opcodes = { {0x00, "PUSH_0"}, {0x01, "PUSH_1"}, {0x02, "PUSH_2"}, {0x03, "PUSH_3"}, {0x04, "PUSH_4"}, {0x05, "PUSH_5"}, {0x06, "PUSH_6"}, {0x07, "PUSH_7"}, {0x0D, "RETURN"}, {0x0F, "BREAK"}, {0x13, "END_BLOCK"}, {0x14, "NOT"}, {0x17, "NEG"}, {0x19, "DUP"}, {0x1F, "POP"}, {0x10, "BIT_AND"}, {0x11, "BIT_OR"}, {0x12, "BIT_NOT"}, {0x23, "ADD"}, {0x24, "SUB"}, {0x25, "MUL"}, {0x26, "DIV"}, {0x27, "MOD"}, {0x2C, "EQ"}, {0x2D, "NE"}, {0x2E, "LT"}, {0x2F, "GT"}, {0x30, "LE"}, {0x34, "AND"}, {0x35, "OR"}, {0x36, "XOR"}, {0x28, "GET_VAR"}, {0x33, "SET_VAR"}, {0x1A, "PUSH_STR"}, {0x29, "GET_ARG"}, {0x2A, "GET_LOCAL"}, {0x2B, "SET_LOCAL"}, {0x32, "INIT_VAR"}, {0x1B, "JUMP"}, {0x1D, "JUMP_COND"}, {0x20, "JMP_FALSE"}, {0x21, "JMP_TRUE"}, {0x31, "CALL"}, {0x38, "ARRAY_GET"}, {0x39, "ARRAY_SET"}, {0x3C, "CONCAT"}, {0x3E, "MEMBER_GET"}, {0x3F, "MEMBER_SET"}, {0x47, "END_FUNC"}, }; if (opcodes.contains(op)) { return opcodes[op]; } return QString("OP_%1").arg(op, 2, 16, QChar('0')).toUpper(); } // opcode_has_operand(opcode) - check if opcode takes an operand if (c.fn == "opcode_has_operand") { if (c.args.size() != 1) throw std::runtime_error("opcode_has_operand(opcode) takes 1 arg"); int op = evalExpr(rt, *c.args[0]).toInt(); static const QSet with_operand = { 0x28, 0x33, 0x1A, 0x29, 0x2A, 0x2B, 0x32, 0x1B, 0x1D, 0x20, 0x21, 0x31, 0x3E, 0x3F }; return with_operand.contains(op) ? 1 : 0; } // Stack simulation for GML decompiler static QStringList s_gmlStack; if (c.fn == "gml_stack_reset") { s_gmlStack.clear(); return 0; } if (c.fn == "gml_stack_push") { if (c.args.size() != 1) throw std::runtime_error("gml_stack_push(value) takes 1 arg"); s_gmlStack.append(evalExpr(rt, *c.args[0]).toString()); return s_gmlStack.size(); } if (c.fn == "gml_stack_pop") { if (s_gmlStack.isEmpty()) return QString("???"); return s_gmlStack.takeLast(); } if (c.fn == "gml_stack_peek") { if (s_gmlStack.isEmpty()) return QString("???"); return s_gmlStack.last(); } if (c.fn == "gml_stack_size") { return s_gmlStack.size(); } throw std::runtime_error(("Unknown function: " + c.fn).toStdString()); } QVariant Interpreter::evalPipe(Runtime& rt, const Expr::Pipe& p) const { QVariant v = evalExpr(rt, *p.base); for (const auto& stage : p.stages) { if (stage.fn == "zlib") { if (!v.canConvert()) throw std::runtime_error("zlib stage expects bytes"); const QByteArray in = v.toByteArray(); const QByteArray decompressed = Compression::DecompressZLIB(in); // Match evalCall("zlib") behavior if (!rt.filePath.isEmpty()) { QFileInfo fi(rt.filePath); const QString stem = fi.completeBaseName(); rt.vars["_zlib_stem"] = stem; // Base name for decompressed output } v = decompressed; continue; } if (stage.fn == "xmem") { if (!v.canConvert()) throw std::runtime_error("xmem stage expects bytes"); const QByteArray in = v.toByteArray(); const QByteArray decompressed = Compression::DecompressXMem(in); if (!rt.filePath.isEmpty()) { QFileInfo fi(rt.filePath); const QString stem = fi.completeBaseName(); rt.vars["_xmem_stem"] = stem; // Base name for decompressed output } v = decompressed; continue; } if (stage.fn == "deflate") { if (!v.canConvert()) throw std::runtime_error("deflate stage expects bytes"); const QByteArray in = v.toByteArray(); const QByteArray decompressed = Compression::DecompressDeflate(in); v = decompressed; continue; } if (stage.fn == "parse") { // stage args[0] is a String expr holding type name (we created it that way in parser) if (stage.args.size() != 1) throw std::runtime_error("parse stage requires type name"); const QString typeName = stage.args[0]->node.index() == 1 ? std::get(stage.args[0]->node).v : evalExpr(rt, *stage.args[0]).toString(); if (!v.canConvert()) throw std::runtime_error("parse stage expects bytes"); const QByteArray bytes = v.toByteArray(); QDataStream nested(bytes); QVariantMap obj = runTypeInternal(typeName, nested, rt.filePath, rt.order); // propagate zlib stem just like evalCall("parse") if (rt.vars.contains("_zlib_stem") && !obj.contains("_zlib_stem")) { obj["_zlib_stem"] = rt.vars["_zlib_stem"]; } DslKeys::set(obj, DslKey::Type, typeName); v = obj; continue; } // Allow calling a stage as fn(current) for future extensions // e.g. stage "zlib" already handled above throw std::runtime_error(("Unknown pipeline stage: " + stage.fn).toStdString()); } return v; } QVariant Interpreter::evalExpr(Runtime& rt, const Expr& e) const { if (std::holds_alternative(e.node)) { return std::get(e.node).v; } if (std::holds_alternative(e.node)) { return std::get(e.node).v; } if (std::holds_alternative(e.node)) { const QString name = std::get(e.node).name; // EOF is a sentinel used only inside read(...) if (name == "EOF") return QString("EOF"); // Check local vars first, then globals if (rt.vars.contains(name)) { return rt.vars.value(name); } if (rt.module && rt.module->globals.contains(name)) { return rt.module->globals.value(name); } throw std::runtime_error(("Unknown variable: " + name).toStdString()); } if (std::holds_alternative(e.node)) { const auto& u = std::get(e.node); QVariant rhs = evalExpr(rt, *u.rhs); if (u.op == "!") return !toBool(rhs); if (u.op == "-") return -toInt(rhs); if (u.op == "+") return +toInt(rhs); throw std::runtime_error(("Unknown unary op: " + u.op).toStdString()); } if (std::holds_alternative(e.node)) { const auto& b = std::get(e.node); QVariant lv = evalExpr(rt, *b.lhs); QVariant rv = evalExpr(rt, *b.rhs); const QString& op = b.op; // comparisons/logical // For == and !=: use numeric comparison if both are convertible to numbers, // otherwise fall back to QVariant comparison (for strings, maps, etc.) if (op == "==" || op == "!=") { bool lnumeric = lv.canConvert() && (lv.typeId() != QMetaType::QString && lv.typeId() != QMetaType::QVariantMap); bool rnumeric = rv.canConvert() && (rv.typeId() != QMetaType::QString && rv.typeId() != QMetaType::QVariantMap); if (lnumeric && rnumeric) { qint64 lval = toInt(lv); qint64 rval = toInt(rv); return (op == "==") ? (lval == rval) : (lval != rval); } // Non-numeric: use QVariant comparison return (op == "==") ? (lv == rv) : (lv != rv); } if (op == "<") return toInt(lv) < toInt(rv); if (op == "<=") return toInt(lv) <= toInt(rv); if (op == ">") return toInt(lv) > toInt(rv); if (op == ">=") return toInt(lv) >= toInt(rv); if (op == "&&") return toBool(lv) && toBool(rv); if (op == "||") return toBool(lv) || toBool(rv); // Handle string concatenation for + if (op == "+") { // If either operand is a string, do string concatenation if (lv.typeId() == QMetaType::QString || rv.typeId() == QMetaType::QString) { return lv.toString() + rv.toString(); } // Otherwise do integer addition return toInt(lv) + toInt(rv); } // arithmetic/bitwise const qint64 a = toInt(lv); const qint64 c = toInt(rv); if (op == "-") return a - c; if (op == "*") return a * c; if (op == "/") return c == 0 ? 0 : (a / c); if (op == "%") return c == 0 ? 0 : (a % c); if (op == "<<") return a << c; if (op == ">>") return a >> c; if (op == "&") return a & c; if (op == "^") return a ^ c; if (op == "|") return a | c; throw std::runtime_error(("Unknown binary op: " + op).toStdString()); } if (std::holds_alternative(e.node)) { return evalCall(rt, std::get(e.node)); } if (std::holds_alternative(e.node)) { const auto& m = std::get(e.node); QVariant obj = evalExpr(rt, *m.object); if (obj.typeId() != QMetaType::QVariantMap) { throw std::runtime_error(("Cannot access field '" + m.field + "' on non-object").toStdString()); } QVariantMap map = obj.toMap(); if (!map.contains(m.field)) { return QVariant(); // Return null for missing fields } return map.value(m.field); } if (std::holds_alternative(e.node)) { return evalPipe(rt, std::get(e.node)); } throw std::runtime_error("Unknown expression node"); } void Interpreter::execBlock(Runtime& rt, const QVector& body) const { for (const auto& s : body) { execStmt(rt, *s); reportProgress(rt); } } void Interpreter::execStmt(Runtime& rt, const Stmt& s) const { if (std::holds_alternative(s.node)) { const auto& rs = std::get(s.node); QVariant v = readScalar(rt, rs.type); rt.vars[rs.name] = v; return; } if (std::holds_alternative(s.node)) { const auto& sk = std::get(s.node); const qint64 n = toInt(evalExpr(rt, *sk.count)); if (n < 0) throw std::runtime_error("skip n must be >= 0"); const qint64 pos = rt.in->device() ? rt.in->device()->pos() : -1; const int skipped = rt.in->skipRawData(int(n)); if (skipped != int(n)) { throw std::runtime_error( QString("Unexpected EOF during skip(%1) at pos 0x%2. Parsing: %3") .arg(n).arg(pos, 0, 16) .arg(rt.typeStackString().isEmpty() ? "root" : rt.typeStackString()) .toStdString()); } return; } if (std::holds_alternative(s.node)) { const auto& al = std::get(s.node); const qint64 n = toInt(evalExpr(rt, *al.n)); if (n <= 0) throw std::runtime_error("align(n): n must be > 0"); QIODevice* dev = rt.in->device(); const qint64 pos = dev->pos(); const qint64 mod = pos % n; if (mod != 0) { const qint64 pad = n - mod; const int skipped = rt.in->skipRawData(int(pad)); if (skipped != int(pad)) { throw std::runtime_error( QString("Unexpected EOF during align(%1) at pos 0x%2. Parsing: %3") .arg(n).arg(pos, 0, 16) .arg(rt.typeStackString().isEmpty() ? "root" : rt.typeStackString()) .toStdString()); } } return; } if (std::holds_alternative(s.node)) { const auto& se = std::get(s.node); const qint64 p = toInt(evalExpr(rt, *se.pos)); if (p < 0) throw std::runtime_error("seek(pos): position must be >= 0"); QIODevice* dev = rt.in->device(); if (!dev->seek(p)) throw std::runtime_error("seek(pos) failed"); return; } if (std::holds_alternative(s.node)) { const auto& as = std::get(s.node); QVariant v = evalExpr(rt, *as.value); rt.vars[as.name] = v; // Store hidden flag if set if (as.ui.hidden) { DslKeys::set(rt.vars, DslKey::Hidden, true); } return; } if (std::holds_alternative(s.node)) { const auto& iff = std::get(s.node); const bool cond = toBool(evalExpr(rt, *iff.cond)); execBlock(rt, cond ? iff.thenBody : iff.elseBody); return; } if (std::holds_alternative(s.node)) { const auto& wh = std::get(s.node); try { while (toBool(evalExpr(rt, *wh.cond))) { execBlock(rt, wh.body); } } catch (const BreakException&) { // break statement encountered, exit loop } return; } if (std::holds_alternative(s.node)) { const auto& rp = std::get(s.node); qint64 count = toInt(evalExpr(rt, *rp.count)); if (count < 0) count = 0; const qint64 startPos = rt.in->device() ? rt.in->device()->pos() : -1; LogManager::instance().debug(QString("[REPEAT] Starting repeat loop: count=%1, pos=0x%2") .arg(count) .arg(startPos, 0, 16)); // provide _i for repeat index const QVariant old = rt.vars.value("_i"); const bool hadOld = rt.vars.contains("_i"); try { for (qint64 i = 0; i < count; i++) { const qint64 iterPos = rt.in->device() ? rt.in->device()->pos() : -1; LogManager::instance().debug(QString("[REPEAT] Iteration %1/%2 at pos 0x%3") .arg(i) .arg(count) .arg(iterPos, 0, 16)); rt.vars["_i"] = i; execBlock(rt, rp.body); } } catch (const BreakException&) { // break statement encountered, exit loop } const qint64 endPos = rt.in->device() ? rt.in->device()->pos() : -1; LogManager::instance().debug(QString("[REPEAT] Finished repeat loop at pos 0x%1") .arg(endPos, 0, 16)); if (hadOld) rt.vars["_i"] = old; else rt.vars.remove("_i"); return; } if (std::holds_alternative(s.node)) { const auto& fr = std::get(s.node); qint64 start = toInt(evalExpr(rt, *fr.start)); qint64 end = toInt(evalExpr(rt, *fr.end)); if (end < start) end = start; const QVariant old = rt.vars.value(fr.var); const bool hadOld = rt.vars.contains(fr.var); try { for (qint64 i = start; i < end; i++) { rt.vars[fr.var] = i; execBlock(rt, fr.body); } } catch (const BreakException&) { // break statement encountered, exit loop } if (hadOld) rt.vars[fr.var] = old; else rt.vars.remove(fr.var); return; } if (std::holds_alternative(s.node)) { const auto& rq = std::get(s.node); const bool ok = toBool(evalExpr(rt, *rq.cond)); if (!ok) throw std::runtime_error("criteria require failed"); return; } if (std::holds_alternative(s.node)) { const auto& bo = std::get(s.node); rt.order = bo.order; applyByteOrder(rt); return; } if (std::holds_alternative(s.node)) { const auto& cs = std::get(s.node); evalExpr(rt, *cs.call); // Execute function, discard result return; } if (std::holds_alternative(s.node)) { throw BreakException{}; } if (std::holds_alternative(s.node)) { const auto& inl = std::get(s.node); // Check if pointer field equals -1 (inline data follows) if (!rt.vars.contains(inl.ptrField)) { throw std::runtime_error(("Inline: pointer field '" + inl.ptrField + "' not found").toStdString()); } qint64 ptrVal = toInt(rt.vars.value(inl.ptrField)); if (ptrVal == -1) { QVariant result; // Handle built-in types if (inl.typeName == "cstring") { // Read null-terminated string QByteArray buf; char ch; while (true) { if (rt.in->readRawData(&ch, 1) != 1) { throw std::runtime_error("cstring: unexpected EOF"); } if (ch == '\0') break; buf.append(ch); } result = QString::fromUtf8(buf); } else { // Parse as type using parse_here if (!rt.module->types.contains(inl.typeName)) { throw std::runtime_error(("Inline: unknown type '" + inl.typeName + "'").toStdString()); } const TypeDef& td = rt.module->types[inl.typeName]; // Save and apply byte order const auto oldOrder = rt.order; ByteOrder childOrder = oldOrder; if (td.hasExplicitByteOrder) childOrder = td.order; Runtime childRt; childRt.in = rt.in; childRt.module = rt.module; childRt.order = childOrder; childRt.typeStack = rt.typeStack; childRt.filePath = rt.filePath; // Copy file variables if (DslKeys::contains(rt.vars, DslKey::Path)) DslKeys::set(childRt.vars, DslKey::Path, DslKeys::get(rt.vars, DslKey::Path)); if (DslKeys::contains(rt.vars, DslKey::Name)) DslKeys::set(childRt.vars, DslKey::Name, DslKeys::get(rt.vars, DslKey::Name)); if (DslKeys::contains(rt.vars, DslKey::Basename)) DslKeys::set(childRt.vars, DslKey::Basename, DslKeys::get(rt.vars, DslKey::Basename)); if (DslKeys::contains(rt.vars, DslKey::Ext)) DslKeys::set(childRt.vars, DslKey::Ext, DslKeys::get(rt.vars, DslKey::Ext)); applyByteOrder(childRt); execBlock(childRt, td.body); QVariantMap obj = childRt.vars; DslKeys::set(obj, DslKey::Type, inl.typeName); result = obj; // Restore byte order rt.order = oldOrder; applyByteOrder(rt); } // Store result rt.vars[inl.varName] = result; // Apply UI flags if specified if (inl.ui.ui) { QVariantMap uiMeta = rt.vars.value("_ui_meta").toMap(); QVariantMap fieldMeta; fieldMeta["visible"] = true; fieldMeta["readonly"] = true; fieldMeta["display"] = inl.ui.display.isEmpty() ? inl.varName : inl.ui.display; uiMeta[inl.varName] = fieldMeta; rt.vars["_ui_meta"] = uiMeta; } // Set name if requested if (inl.setName) { DslKeys::set(rt.vars, DslKey::Name, result.toString()); } } return; } if (std::holds_alternative(s.node)) { const auto& arr = std::get(s.node); // Check pointer condition if specified bool shouldParse = true; if (!arr.ptrField.isEmpty()) { if (!rt.vars.contains(arr.ptrField)) { throw std::runtime_error(("Array: pointer field '" + arr.ptrField + "' not found").toStdString()); } qint64 ptrVal = toInt(rt.vars.value(arr.ptrField)); shouldParse = (ptrVal == -1); } if (shouldParse) { qint64 count = toInt(evalExpr(rt, *arr.count)); if (count < 0) count = 0; QVariantList list; if (!rt.module->types.contains(arr.elementType)) { throw std::runtime_error(("Array: unknown element type '" + arr.elementType + "'").toStdString()); } const TypeDef& td = rt.module->types[arr.elementType]; const auto oldOrder = rt.order; ByteOrder childOrder = oldOrder; if (td.hasExplicitByteOrder) childOrder = td.order; for (qint64 i = 0; i < count; i++) { Runtime childRt; childRt.in = rt.in; childRt.module = rt.module; childRt.order = childOrder; childRt.typeStack = rt.typeStack; childRt.filePath = rt.filePath; if (DslKeys::contains(rt.vars, DslKey::Path)) DslKeys::set(childRt.vars, DslKey::Path, DslKeys::get(rt.vars, DslKey::Path)); if (DslKeys::contains(rt.vars, DslKey::Name)) DslKeys::set(childRt.vars, DslKey::Name, DslKeys::get(rt.vars, DslKey::Name)); if (DslKeys::contains(rt.vars, DslKey::Basename)) DslKeys::set(childRt.vars, DslKey::Basename, DslKeys::get(rt.vars, DslKey::Basename)); if (DslKeys::contains(rt.vars, DslKey::Ext)) DslKeys::set(childRt.vars, DslKey::Ext, DslKeys::get(rt.vars, DslKey::Ext)); applyByteOrder(childRt); execBlock(childRt, td.body); QVariantMap obj = childRt.vars; DslKeys::set(obj, DslKey::Type, arr.elementType); list.append(obj); } rt.order = oldOrder; applyByteOrder(rt); rt.vars[arr.varName] = list; // Apply UI flags if (arr.ui.ui) { QVariantMap uiMeta = rt.vars.value("_ui_meta").toMap(); QVariantMap fieldMeta; fieldMeta["visible"] = true; fieldMeta["readonly"] = true; fieldMeta["display"] = arr.ui.display.isEmpty() ? arr.varName : arr.ui.display; if (!arr.ui.tableTitle.isEmpty()) { fieldMeta["table"] = arr.ui.tableTitle; fieldMeta["columns"] = arr.ui.columnsCsv; } uiMeta[arr.varName] = fieldMeta; rt.vars["_ui_meta"] = uiMeta; } } return; } if (std::holds_alternative(s.node)) { const auto& c = std::get(s.node); QVariant val = evalExpr(rt, *c.value); // Store as a constant (same as regular variable for now) rt.vars[c.name] = val; return; } if (std::holds_alternative(s.node)) { const auto& m = std::get(s.node); QVariant matchVal = evalExpr(rt, *m.expr); bool matched = false; for (const auto& arm : m.arms) { const QVector& patterns = arm.first; const QVector& body = arm.second; for (const auto& pattern : patterns) { QVariant patternVal = evalExpr(rt, *pattern); // Compare values bool isMatch = false; bool lnumeric = matchVal.canConvert() && (matchVal.typeId() != QMetaType::QString && matchVal.typeId() != QMetaType::QVariantMap); bool rnumeric = patternVal.canConvert() && (patternVal.typeId() != QMetaType::QString && patternVal.typeId() != QMetaType::QVariantMap); if (lnumeric && rnumeric) { isMatch = toInt(matchVal) == toInt(patternVal); } else { isMatch = (matchVal == patternVal); } if (isMatch) { execBlock(rt, body); matched = true; break; } } if (matched) break; } // Execute default if no match if (!matched && !m.defaultBody.isEmpty()) { execBlock(rt, m.defaultBody); } return; } throw std::runtime_error("Unknown statement node"); }