2026-01-08 00:38:12 -05:00
|
|
|
// Debug logging through LogManager (controlled by Settings)
|
2026-01-07 16:35:35 -05:00
|
|
|
|
2026-01-01 22:18:25 -05:00
|
|
|
#include "interpreter.h"
|
2026-01-11 12:08:26 -05:00
|
|
|
#include "dslkeys.h"
|
2026-01-07 16:35:35 -05:00
|
|
|
#include <cstring>
|
2026-01-01 22:18:25 -05:00
|
|
|
#include "compression.h"
|
|
|
|
|
#include "utils.h"
|
|
|
|
|
#include "logmanager.h"
|
|
|
|
|
|
|
|
|
|
#include <stdexcept>
|
2026-01-07 16:35:35 -05:00
|
|
|
// Custom exception for break statement
|
|
|
|
|
struct BreakException : std::exception {};
|
2026-01-01 22:18:25 -05:00
|
|
|
#include <QFileInfo>
|
2026-01-11 12:08:26 -05:00
|
|
|
#include <QProcess>
|
|
|
|
|
#include <QCoreApplication>
|
|
|
|
|
#include <QDir>
|
|
|
|
|
#include <QtEndian>
|
2026-01-01 22:18:25 -05:00
|
|
|
|
|
|
|
|
Interpreter::Interpreter(Module mod) : m_mod(std::move(mod)) {}
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
void Interpreter::setProgressCallback(ProgressCallback cb) {
|
|
|
|
|
m_progressCallback = std::move(cb);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Interpreter::setFileSize(qint64 size) {
|
|
|
|
|
m_fileSize = size;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 00:38:12 -05:00
|
|
|
void Interpreter::setStatusCallback(StatusCallback cb) {
|
|
|
|
|
m_statusCallback = std::move(cb);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
void Interpreter::reportProgress(Runtime& rt) const {
|
|
|
|
|
if (m_progressCallback && rt.in && rt.in->device()) {
|
|
|
|
|
m_progressCallback(rt.in->device()->pos(), m_fileSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 00:38:12 -05:00
|
|
|
void Interpreter::reportStatus(const QString& status) const {
|
|
|
|
|
if (m_statusCallback) {
|
|
|
|
|
m_statusCallback(status);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 22:18:25 -05:00
|
|
|
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;
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[PARSE] Starting type '%1' at pos 0x%2")
|
2026-01-01 22:18:25 -05:00
|
|
|
.arg(typeName)
|
|
|
|
|
.arg(startPos, 0, 16));
|
|
|
|
|
|
|
|
|
|
Runtime rt;
|
|
|
|
|
rt.in = &stream;
|
|
|
|
|
rt.module = &m_mod;
|
2026-01-07 16:35:35 -05:00
|
|
|
rt.pushType(typeName); // Track root type
|
2026-01-01 22:18:25 -05:00
|
|
|
|
|
|
|
|
const TypeDef& td = m_mod.types[typeName];
|
|
|
|
|
rt.order = td.order;
|
|
|
|
|
applyByteOrder(rt);
|
|
|
|
|
|
|
|
|
|
if (!filePath.isEmpty())
|
|
|
|
|
{
|
|
|
|
|
seedFileVars(rt, filePath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
execBlock(rt, td.body);
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
DslKeys::set(rt.vars, DslKey::Type, typeName);
|
2026-01-01 22:18:25 -05:00
|
|
|
|
|
|
|
|
const qint64 endPos = stream.device() ? stream.device()->pos() : -1;
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[PARSE] Finished type '%1' at pos 0x%2 (consumed %3 bytes)")
|
2026-01-01 22:18:25 -05:00
|
|
|
.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);
|
2026-01-11 12:08:26 -05:00
|
|
|
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.
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool Interpreter::checkCriteria(const QString &typeName, QIODevice *dev, const QString filePath) const
|
|
|
|
|
{
|
|
|
|
|
if (!m_mod.types.contains(typeName))
|
|
|
|
|
{
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[CRITERIA] Type '%1' not found").arg(typeName));
|
2026-01-01 22:18:25 -05:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const TypeDef& td = m_mod.types[typeName];
|
|
|
|
|
if (td.criteria.isEmpty())
|
|
|
|
|
{
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[CRITERIA] Type '%1' has no criteria, returning true").arg(typeName));
|
2026-01-01 22:18:25 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[CRITERIA] Checking type '%1' for file '%2'").arg(typeName).arg(filePath));
|
2026-01-01 22:18:25 -05:00
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[CRITERIA] Type '%1' PASSED criteria").arg(typeName));
|
2026-01-01 22:18:25 -05:00
|
|
|
return true;
|
|
|
|
|
} catch (const std::exception& e) {
|
|
|
|
|
restore();
|
|
|
|
|
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[CRITERIA] Type '%1' FAILED: %2").arg(typeName).arg(e.what()));
|
2026-01-01 22:18:25 -05:00
|
|
|
return false;
|
|
|
|
|
} catch (...) {
|
|
|
|
|
restore();
|
|
|
|
|
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[CRITERIA] Type '%1' FAILED (unknown exception)").arg(typeName));
|
2026-01-01 22:18:25 -05:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariantMap Interpreter::runTypeInternal(const QString &typeName, QDataStream &stream, const QString &filePath, std::optional<ByteOrder> 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;
|
2026-01-07 16:35:35 -05:00
|
|
|
rt.pushType(typeName); // Track type for error context
|
2026-01-01 22:18:25 -05:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
DslKeys::set(rt.vars, DslKey::Type, typeName);
|
2026-01-01 22:18:25 -05:00
|
|
|
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<qint64>()) 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<qint64>()) return v.toLongLong() != 0;
|
|
|
|
|
if (v.typeId() == QMetaType::QString) return !v.toString().isEmpty();
|
|
|
|
|
if (v.canConvert<QByteArray>()) return !v.toByteArray().isEmpty();
|
|
|
|
|
return v.isValid();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariant Interpreter::readScalar(Runtime& rt, ScalarType t) const {
|
|
|
|
|
QDataStream& in = *rt.in;
|
2026-01-07 16:35:35 -05:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-01 22:18:25 -05:00
|
|
|
switch (t) {
|
2026-01-07 16:35:35 -05:00
|
|
|
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); }
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
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");
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
const qint64 pos = dev->pos();
|
2026-01-01 22:18:25 -05:00
|
|
|
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));
|
2026-01-07 16:35:35 -05:00
|
|
|
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());
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
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);
|
2026-01-11 12:08:26 -05:00
|
|
|
const QString stem = fi.completeBaseName();
|
|
|
|
|
rt.vars["_zlib_stem"] = stem; // Base name for decompressed output
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return decompressed;
|
|
|
|
|
}
|
2026-01-07 16:35:35 -05:00
|
|
|
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();
|
2026-01-08 00:38:12 -05:00
|
|
|
reportStatus(QString("Decompressing %1 bytes...").arg(in.size()));
|
2026-01-07 16:35:35 -05:00
|
|
|
const QByteArray decompressed = Compression::DecompressXMem(in);
|
|
|
|
|
|
|
|
|
|
if (!rt.filePath.isEmpty()) {
|
|
|
|
|
QFileInfo fi(rt.filePath);
|
|
|
|
|
const QString stem = fi.completeBaseName();
|
2026-01-11 12:08:26 -05:00
|
|
|
rt.vars["_xmem_stem"] = stem; // Base name for decompressed output
|
2026-01-07 16:35:35 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-08 00:38:12 -05:00
|
|
|
reportStatus(QString("Decompressed to %1 bytes").arg(decompressed.size()));
|
2026-01-07 16:35:35 -05:00
|
|
|
return decompressed;
|
|
|
|
|
}
|
2026-01-11 12:08:26 -05:00
|
|
|
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;
|
|
|
|
|
}
|
2026-01-01 22:18:25 -05:00
|
|
|
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();
|
|
|
|
|
|
2026-01-08 00:38:12 -05:00
|
|
|
reportStatus(QString("Parsing %1...").arg(typeName));
|
2026-01-01 22:18:25 -05:00
|
|
|
QDataStream nested(bytes);
|
|
|
|
|
|
|
|
|
|
QVariantMap obj = runTypeInternal(typeName, nested, rt.filePath, rt.order);
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
if (rt.vars.contains("_zlib_stem") && !obj.contains("_zlib_stem")) {
|
|
|
|
|
obj["_zlib_stem"] = rt.vars["_zlib_stem"];
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
// Only set _type if not already set (parse_here delegation sets it)
|
|
|
|
|
if (!DslKeys::contains(obj, DslKey::Type)) {
|
|
|
|
|
DslKeys::set(obj, DslKey::Type, typeName);
|
|
|
|
|
}
|
2026-01-01 22:18:25 -05:00
|
|
|
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;
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[PARSE_HERE] Parsing '%1' at pos 0x%2")
|
2026-01-01 22:18:25 -05:00
|
|
|
.arg(typeName)
|
|
|
|
|
.arg(startPos, 0, 16));
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
// Push type onto stack for error context
|
|
|
|
|
rt.pushType(typeName);
|
|
|
|
|
|
2026-01-01 22:18:25 -05:00
|
|
|
// 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;
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
// Create child runtime sharing stream but inheriting type stack
|
2026-01-01 22:18:25 -05:00
|
|
|
Runtime childRt;
|
|
|
|
|
childRt.in = rt.in;
|
|
|
|
|
childRt.module = rt.module;
|
|
|
|
|
childRt.order = childOrder;
|
2026-01-07 16:35:35 -05:00
|
|
|
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
|
2026-01-11 12:08:26 -05:00
|
|
|
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));
|
2026-01-07 16:35:35 -05:00
|
|
|
|
2026-01-01 22:18:25 -05:00
|
|
|
applyByteOrder(childRt);
|
|
|
|
|
|
|
|
|
|
execBlock(childRt, td.body);
|
|
|
|
|
QVariantMap child = childRt.vars;
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
// Pop type from stack
|
|
|
|
|
rt.popType();
|
|
|
|
|
|
2026-01-01 22:18:25 -05:00
|
|
|
// Restore parent order
|
|
|
|
|
rt.order = oldOrder;
|
|
|
|
|
applyByteOrder(rt);
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
// Merge child variables into parent context (for delegation pattern)
|
|
|
|
|
// Skip reserved metadata variables - parent controls these
|
|
|
|
|
static const QSet<QString> 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);
|
2026-01-01 22:18:25 -05:00
|
|
|
|
|
|
|
|
const qint64 endPos = rt.in->device() ? rt.in->device()->pos() : -1;
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[PARSE_HERE] Finished '%1' at pos 0x%2 (consumed %3 bytes)")
|
2026-01-01 22:18:25 -05:00
|
|
|
.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();
|
2026-01-11 12:08:26 -05:00
|
|
|
const QString itemType = DslKeys::getString(itemMap, DslKey::Type);
|
|
|
|
|
const QString itemName = DslKeys::getString(itemMap, DslKey::Name);
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[PUSH] Added item #%1 to '%2': type='%3', name='%4'")
|
2026-01-01 22:18:25 -05:00
|
|
|
.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()) {
|
2026-01-07 16:35:35 -05:00
|
|
|
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());
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
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)) {
|
2026-01-07 16:35:35 -05:00
|
|
|
throw std::runtime_error(QString("get: field '%1' not found in object. Parsing: %2")
|
|
|
|
|
.arg(fieldName)
|
|
|
|
|
.arg(rt.typeStackString().isEmpty() ? "root" : rt.typeStackString())
|
|
|
|
|
.toStdString());
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 22:18:25 -05:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 22:18:25 -05:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
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<quint8>(b[i]) << 8) | static_cast<quint8>(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<quint8>(b[i]) | (static_cast<quint8>(b[i + 1]) << 8);
|
|
|
|
|
if (ch == 0) break; // null terminator
|
|
|
|
|
result.append(QChar(ch));
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 22:18:25 -05:00
|
|
|
if (c.fn == "cstring") {
|
|
|
|
|
if (c.args.size() != 0) throw std::runtime_error("cstring() takes 0 args");
|
|
|
|
|
// Read null-terminated string (C-style string)
|
2026-01-07 16:35:35 -05:00
|
|
|
const qint64 startPos = rt.in->device() ? rt.in->device()->pos() : -1;
|
2026-01-01 22:18:25 -05:00
|
|
|
QByteArray result;
|
|
|
|
|
char ch;
|
|
|
|
|
while (true) {
|
|
|
|
|
if (rt.in->readRawData(&ch, 1) != 1) {
|
2026-01-07 16:35:35 -05:00
|
|
|
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());
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
if (ch == '\0') break;
|
|
|
|
|
result.append(ch);
|
|
|
|
|
}
|
|
|
|
|
return QString::fromUtf8(result);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
// 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<char*>(&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<char*>(&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");
|
2026-01-01 22:18:25 -05:00
|
|
|
const QString name = evalExpr(rt, *c.args[0]).toString();
|
|
|
|
|
const QVariant value = evalExpr(rt, *c.args[1]);
|
|
|
|
|
rt.module->globals[name] = value;
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
if (c.fn == "ctx_get") {
|
|
|
|
|
if (c.args.size() != 1) throw std::runtime_error("ctx_get(name) takes 1 arg");
|
2026-01-08 00:38:12 -05:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
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<qint64>() &&
|
|
|
|
|
(value.typeId() != QMetaType::QString && value.typeId() != QMetaType::QVariantMap);
|
|
|
|
|
bool rnumeric = caseVal.canConvert<qint64>() &&
|
|
|
|
|
(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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 22:18:25 -05:00
|
|
|
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();
|
|
|
|
|
|
2026-01-08 00:38:12 -05:00
|
|
|
reportStatus(QString("Exporting %1...").arg(filename));
|
|
|
|
|
|
2026-01-01 22:18:25 -05:00
|
|
|
// Use Utils::ExportData which writes to exports/ directory
|
|
|
|
|
if (!Utils::ExportData(filename, data)) {
|
|
|
|
|
throw std::runtime_error(("Failed to export file: " + filename).toStdString());
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[EXPORT] Exported %1 bytes to exports/%2")
|
2026-01-01 22:18:25 -05:00
|
|
|
.arg(data.size())
|
|
|
|
|
.arg(filename));
|
|
|
|
|
return data.size(); // Return number of bytes written
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 00:38:12 -05:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 00:38:12 -05:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
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();
|
2026-01-11 12:08:26 -05:00
|
|
|
DslKeys::set(rt.vars, DslKey::Preview, preview);
|
2026-01-07 16:35:35 -05:00
|
|
|
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[PREVIEW] Set preview: %1 (%2 bytes)")
|
2026-01-07 16:35:35 -05:00
|
|
|
.arg(filename)
|
|
|
|
|
.arg(data.size()));
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
// 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<int, QString> 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<int> 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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 22:18:25 -05:00
|
|
|
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<QByteArray>()) 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);
|
2026-01-11 12:08:26 -05:00
|
|
|
const QString stem = fi.completeBaseName();
|
|
|
|
|
rt.vars["_zlib_stem"] = stem; // Base name for decompressed output
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
v = decompressed;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
if (stage.fn == "xmem") {
|
|
|
|
|
if (!v.canConvert<QByteArray>()) 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();
|
2026-01-11 12:08:26 -05:00
|
|
|
rt.vars["_xmem_stem"] = stem; // Base name for decompressed output
|
2026-01-07 16:35:35 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
v = decompressed;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-01-11 12:08:26 -05:00
|
|
|
if (stage.fn == "deflate") {
|
|
|
|
|
if (!v.canConvert<QByteArray>()) throw std::runtime_error("deflate stage expects bytes");
|
|
|
|
|
|
|
|
|
|
const QByteArray in = v.toByteArray();
|
|
|
|
|
const QByteArray decompressed = Compression::DecompressDeflate(in);
|
|
|
|
|
|
|
|
|
|
v = decompressed;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-01-07 16:35:35 -05:00
|
|
|
|
2026-01-01 22:18:25 -05:00
|
|
|
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<Expr::String>(stage.args[0]->node).v
|
|
|
|
|
: evalExpr(rt, *stage.args[0]).toString();
|
|
|
|
|
|
|
|
|
|
if (!v.canConvert<QByteArray>()) 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);
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
// propagate zlib stem just like evalCall("parse")
|
|
|
|
|
if (rt.vars.contains("_zlib_stem") && !obj.contains("_zlib_stem")) {
|
|
|
|
|
obj["_zlib_stem"] = rt.vars["_zlib_stem"];
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
DslKeys::set(obj, DslKey::Type, typeName);
|
2026-01-01 22:18:25 -05:00
|
|
|
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<Expr::Int>(e.node)) {
|
|
|
|
|
return std::get<Expr::Int>(e.node).v;
|
|
|
|
|
}
|
|
|
|
|
if (std::holds_alternative<Expr::String>(e.node)) {
|
|
|
|
|
return std::get<Expr::String>(e.node).v;
|
|
|
|
|
}
|
|
|
|
|
if (std::holds_alternative<Expr::Var>(e.node)) {
|
|
|
|
|
const QString name = std::get<Expr::Var>(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<Expr::Unary>(e.node)) {
|
|
|
|
|
const auto& u = std::get<Expr::Unary>(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<Expr::Binary>(e.node)) {
|
|
|
|
|
const auto& b = std::get<Expr::Binary>(e.node);
|
|
|
|
|
QVariant lv = evalExpr(rt, *b.lhs);
|
|
|
|
|
QVariant rv = evalExpr(rt, *b.rhs);
|
|
|
|
|
|
|
|
|
|
const QString& op = b.op;
|
|
|
|
|
|
|
|
|
|
// comparisons/logical
|
2026-01-07 16:35:35 -05:00
|
|
|
// 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<qint64>() &&
|
|
|
|
|
(lv.typeId() != QMetaType::QString && lv.typeId() != QMetaType::QVariantMap);
|
|
|
|
|
bool rnumeric = rv.canConvert<qint64>() &&
|
|
|
|
|
(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);
|
|
|
|
|
}
|
2026-01-01 22:18:25 -05:00
|
|
|
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<Expr::Call>(e.node)) {
|
|
|
|
|
return evalCall(rt, std::get<Expr::Call>(e.node));
|
|
|
|
|
}
|
2026-01-11 12:08:26 -05:00
|
|
|
if (std::holds_alternative<Expr::Member>(e.node)) {
|
|
|
|
|
const auto& m = std::get<Expr::Member>(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);
|
|
|
|
|
}
|
2026-01-01 22:18:25 -05:00
|
|
|
if (std::holds_alternative<Expr::Pipe>(e.node)) {
|
|
|
|
|
return evalPipe(rt, std::get<Expr::Pipe>(e.node));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw std::runtime_error("Unknown expression node");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Interpreter::execBlock(Runtime& rt, const QVector<StmtPtr>& body) const {
|
2026-01-07 16:35:35 -05:00
|
|
|
for (const auto& s : body) {
|
|
|
|
|
execStmt(rt, *s);
|
|
|
|
|
reportProgress(rt);
|
|
|
|
|
}
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Interpreter::execStmt(Runtime& rt, const Stmt& s) const {
|
|
|
|
|
if (std::holds_alternative<Stmt::ReadScalar>(s.node)) {
|
|
|
|
|
const auto& rs = std::get<Stmt::ReadScalar>(s.node);
|
|
|
|
|
QVariant v = readScalar(rt, rs.type);
|
|
|
|
|
rt.vars[rs.name] = v;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (std::holds_alternative<Stmt::Skip>(s.node)) {
|
|
|
|
|
const auto& sk = std::get<Stmt::Skip>(s.node);
|
|
|
|
|
const qint64 n = toInt(evalExpr(rt, *sk.count));
|
|
|
|
|
if (n < 0) throw std::runtime_error("skip n must be >= 0");
|
2026-01-07 16:35:35 -05:00
|
|
|
const qint64 pos = rt.in->device() ? rt.in->device()->pos() : -1;
|
2026-01-01 22:18:25 -05:00
|
|
|
const int skipped = rt.in->skipRawData(int(n));
|
2026-01-07 16:35:35 -05:00
|
|
|
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());
|
|
|
|
|
}
|
2026-01-01 22:18:25 -05:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (std::holds_alternative<Stmt::Align>(s.node)) {
|
|
|
|
|
const auto& al = std::get<Stmt::Align>(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));
|
2026-01-07 16:35:35 -05:00
|
|
|
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());
|
|
|
|
|
}
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (std::holds_alternative<Stmt::Seek>(s.node)) {
|
|
|
|
|
const auto& se = std::get<Stmt::Seek>(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<Stmt::Assign>(s.node)) {
|
|
|
|
|
const auto& as = std::get<Stmt::Assign>(s.node);
|
|
|
|
|
QVariant v = evalExpr(rt, *as.value);
|
|
|
|
|
rt.vars[as.name] = v;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Store hidden flag if set
|
|
|
|
|
if (as.ui.hidden) {
|
2026-01-11 12:08:26 -05:00
|
|
|
DslKeys::set(rt.vars, DslKey::Hidden, true);
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (std::holds_alternative<Stmt::If>(s.node)) {
|
|
|
|
|
const auto& iff = std::get<Stmt::If>(s.node);
|
|
|
|
|
const bool cond = toBool(evalExpr(rt, *iff.cond));
|
|
|
|
|
execBlock(rt, cond ? iff.thenBody : iff.elseBody);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (std::holds_alternative<Stmt::While>(s.node)) {
|
|
|
|
|
const auto& wh = std::get<Stmt::While>(s.node);
|
2026-01-07 16:35:35 -05:00
|
|
|
try {
|
|
|
|
|
while (toBool(evalExpr(rt, *wh.cond))) {
|
|
|
|
|
execBlock(rt, wh.body);
|
|
|
|
|
}
|
|
|
|
|
} catch (const BreakException&) {
|
|
|
|
|
// break statement encountered, exit loop
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (std::holds_alternative<Stmt::Repeat>(s.node)) {
|
|
|
|
|
const auto& rp = std::get<Stmt::Repeat>(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;
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[REPEAT] Starting repeat loop: count=%1, pos=0x%2")
|
2026-01-01 22:18:25 -05:00
|
|
|
.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");
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
try {
|
|
|
|
|
for (qint64 i = 0; i < count; i++) {
|
|
|
|
|
const qint64 iterPos = rt.in->device() ? rt.in->device()->pos() : -1;
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[REPEAT] Iteration %1/%2 at pos 0x%3")
|
2026-01-07 16:35:35 -05:00
|
|
|
.arg(i)
|
|
|
|
|
.arg(count)
|
|
|
|
|
.arg(iterPos, 0, 16));
|
|
|
|
|
rt.vars["_i"] = i;
|
|
|
|
|
execBlock(rt, rp.body);
|
|
|
|
|
}
|
|
|
|
|
} catch (const BreakException&) {
|
|
|
|
|
// break statement encountered, exit loop
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const qint64 endPos = rt.in->device() ? rt.in->device()->pos() : -1;
|
2026-01-08 00:38:12 -05:00
|
|
|
LogManager::instance().debug(QString("[REPEAT] Finished repeat loop at pos 0x%1")
|
2026-01-01 22:18:25 -05:00
|
|
|
.arg(endPos, 0, 16));
|
|
|
|
|
|
|
|
|
|
if (hadOld) rt.vars["_i"] = old;
|
|
|
|
|
else rt.vars.remove("_i");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (std::holds_alternative<Stmt::ForRange>(s.node)) {
|
|
|
|
|
const auto& fr = std::get<Stmt::ForRange>(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);
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
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
|
2026-01-01 22:18:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hadOld) rt.vars[fr.var] = old;
|
|
|
|
|
else rt.vars.remove(fr.var);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (std::holds_alternative<Stmt::Require>(s.node)) {
|
|
|
|
|
const auto& rq = std::get<Stmt::Require>(s.node);
|
|
|
|
|
const bool ok = toBool(evalExpr(rt, *rq.cond));
|
|
|
|
|
if (!ok) throw std::runtime_error("criteria require failed");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (std::holds_alternative<Stmt::SetByteOrder>(s.node)) {
|
|
|
|
|
const auto& bo = std::get<Stmt::SetByteOrder>(s.node);
|
|
|
|
|
rt.order = bo.order;
|
|
|
|
|
applyByteOrder(rt);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (std::holds_alternative<Stmt::CallStmt>(s.node)) {
|
|
|
|
|
const auto& cs = std::get<Stmt::CallStmt>(s.node);
|
|
|
|
|
evalExpr(rt, *cs.call); // Execute function, discard result
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 16:35:35 -05:00
|
|
|
if (std::holds_alternative<Stmt::Break>(s.node)) {
|
|
|
|
|
throw BreakException{};
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-11 12:08:26 -05:00
|
|
|
if (std::holds_alternative<Stmt::Inline>(s.node)) {
|
|
|
|
|
const auto& inl = std::get<Stmt::Inline>(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<Stmt::ArrayDecl>(s.node)) {
|
|
|
|
|
const auto& arr = std::get<Stmt::ArrayDecl>(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<Stmt::Const>(s.node)) {
|
|
|
|
|
const auto& c = std::get<Stmt::Const>(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<Stmt::Match>(s.node)) {
|
|
|
|
|
const auto& m = std::get<Stmt::Match>(s.node);
|
|
|
|
|
QVariant matchVal = evalExpr(rt, *m.expr);
|
|
|
|
|
|
|
|
|
|
bool matched = false;
|
|
|
|
|
for (const auto& arm : m.arms) {
|
|
|
|
|
const QVector<ExprPtr>& patterns = arm.first;
|
|
|
|
|
const QVector<StmtPtr>& body = arm.second;
|
|
|
|
|
|
|
|
|
|
for (const auto& pattern : patterns) {
|
|
|
|
|
QVariant patternVal = evalExpr(rt, *pattern);
|
|
|
|
|
|
|
|
|
|
// Compare values
|
|
|
|
|
bool isMatch = false;
|
|
|
|
|
bool lnumeric = matchVal.canConvert<qint64>() &&
|
|
|
|
|
(matchVal.typeId() != QMetaType::QString && matchVal.typeId() != QMetaType::QVariantMap);
|
|
|
|
|
bool rnumeric = patternVal.canConvert<qint64>() &&
|
|
|
|
|
(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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-01 22:18:25 -05:00
|
|
|
throw std::runtime_error("Unknown statement node");
|
|
|
|
|
}
|