Add annotation filter function to event browser filters

This commit is contained in:
baldurk
2026-01-28 14:42:10 +00:00
parent 50a0d9c425
commit 1bec491d90
+297 -2
View File
@@ -1501,6 +1501,7 @@ public:
MAKE_BUILTIN_FILTER(dispatch);
MAKE_BUILTIN_FILTER(childOf);
MAKE_BUILTIN_FILTER(parent);
MAKE_BUILTIN_FILTER(annot);
/*
m_BuiltinFilters[lit("event")].completer = [this](ICaptureContext *ctx, QString name,
@@ -1686,11 +1687,11 @@ private:
QString text;
};
static QList<Token> tokenise(QString parameters)
static QList<Token> tokenise(QString parameters, QString extraChars = QString())
{
QList<Token> ret;
const QString operatorChars = lit("<>=:&");
const QString operatorChars = lit("<>=:&") + extraChars;
int p = 0;
while(p < parameters.size())
@@ -1883,6 +1884,300 @@ searched for as a case-insensitive substring.
};
}
QString filterDescription_annot() const
{
return tr(R"EOD(
<h3>$annot</h3>
<br />
<table>
<tr><td><code>$annot(annotation condition)</code><br />
<code>$annot(annotation condition)</code></td><td> - passes if a given annotation condition is true.</td></tr>
</table>
<p>
This filter queries the annotations of an event to either exist or match simple conditions. A
condition may be omitted, but an annotation must be specified. If no condition is given then
the filter passes if that annotation is exists</code>.
</p>
<p>
If the annotation contains spaces, it should be surrounded with double quotes, e.g. "foo bar.sub.key"
</p>
<p>
For numeric annotations you can compare with the normal comparison operators. For string annotations you
can check exact equality with ==, use the keyword 'contains' to check for a case-insensitive substring
match, or =~ to match with a regex.
</p>
<p>
Numeric properties will always fail to match the filter when used with a string comparison operator,
and vice-versa.
</p>
)EOD",
"EventFilterModel");
}
IEventBrowser::EventFilterCallback filterFunction_annot(QString name, QString parameters,
ParseTrace &trace)
{
parameters = parameters.trimmed();
rdcstr keyPath;
if(parameters[0] == QLatin1Char('"'))
{
int i = 1;
while(i < parameters.size() && parameters[i] != QLatin1Char('"'))
{
if(parameters[i] == QLatin1Char('\\') && parameters[i] == QLatin1Char('"'))
{
keyPath += parameters[i + 1];
i++;
}
i++;
}
if(i >= parameters.size())
{
trace.setError(tr("Invalid quoted annotation, no closing quote found", "EventFilterModel"));
return NULL;
}
keyPath = parameters.mid(0, i);
i++;
parameters = parameters.mid(i).trimmed();
}
else
{
int i = parameters.indexOf(QLatin1Char(' '));
if(i < 0)
{
keyPath = parameters;
parameters.clear();
}
keyPath = parameters.mid(0, i);
parameters = parameters.mid(i).trimmed();
}
QList<Token> tokens = tokenise(parameters, lit("~"));
std::function<bool(const SDObject *)> comparer;
// no parameters, just return if the annotation exists
if(tokens.isEmpty())
{
}
else
{
if(tokens.size() == 1)
{
trace.setError(tr("Invalid annotation expression, expected an operator and value",
"EventFilterModel"));
return NULL;
}
QString op = tokens[0].text;
QString param = parameters.mid(tokens[1].position);
static const QStringList numericOperators = {
lit("="), lit("=="), lit("!="), lit("<"), lit(">"), lit("<="), lit(">="),
};
// any numeric value that can be compared to a number
if(numericOperators.contains(op))
{
int operatorIdx = numericOperators.indexOf(op);
// need to be a bit polymorphic here as we don't know what we'll be comparing against so we
// don't know the type. We at least upcast to the biggest type to reduce variations
int64_t s64 = param.toLongLong();
uint64_t u64 = param.toULongLong();
double d = param.toDouble();
comparer = [operatorIdx, d, u64, s64, param](const SDObject *obj) {
switch(obj->type.basetype)
{
case SDBasic::Chunk:
case SDBasic::Resource:
case SDBasic::GPUAddress:
case SDBasic::Struct:
case SDBasic::Array:
case SDBasic::Null:
case SDBasic::Buffer:
return false;
// ambiguity with string operators here, could be an equality check on strings
case SDBasic::String:
if(operatorIdx <= 1)
return QString(obj->data.str) == param;
return false;
case SDBasic::Boolean:
case SDBasic::Enum:
case SDBasic::UnsignedInteger:
case SDBasic::Character:
{
switch(operatorIdx)
{
case 0:
case 1: return obj->data.basic.u == u64;
case 2: return obj->data.basic.u != u64;
case 3: return obj->data.basic.u < u64;
case 4: return obj->data.basic.u > u64;
case 5: return obj->data.basic.u <= u64;
case 6: return obj->data.basic.u >= u64;
default: return false;
}
}
case SDBasic::SignedInteger:
{
switch(operatorIdx)
{
case 0:
case 1: return obj->data.basic.i == s64;
case 2: return obj->data.basic.i != s64;
case 3: return obj->data.basic.i < s64;
case 4: return obj->data.basic.i > s64;
case 5: return obj->data.basic.i <= s64;
case 6: return obj->data.basic.i >= s64;
default: return false;
}
}
case SDBasic::Float:
{
switch(operatorIdx)
{
case 0:
case 1: return obj->data.basic.d == d;
case 2: return obj->data.basic.d != d;
case 3: return obj->data.basic.d < d;
case 4: return obj->data.basic.d > d;
case 5: return obj->data.basic.d <= d;
case 6: return obj->data.basic.d >= d;
default: return false;
}
}
}
return false;
};
}
else if(op == lit("contains") || op == lit("=~"))
{
bool contains = op == lit("contains");
QRegularExpression regex;
if(!contains)
{
int end = 0;
if(param.size() < 2 || param[0] != QLatin1Char('/'))
{
trace.setError(tr("Parameter to =~ should be /regex/", "EventFilterModel"));
return NULL;
}
for(int i = 1; i < param.size(); i++)
{
if(param[i] == QLatin1Char('\\'))
{
i++;
continue;
}
if(param[i] == QLatin1Char('/'))
{
end = i;
break;
}
}
if(end == 0)
{
trace.setError(tr("Unterminated regex", "EventFilterModel"));
return NULL;
}
QString opts = param.right(param.size() - end - 1);
QRegularExpression::PatternOptions reOpts = QRegularExpression::NoPatternOption;
for(QChar c : opts)
{
switch(c.toLatin1())
{
case 'i': reOpts |= QRegularExpression::CaseInsensitiveOption; break;
case 'x': reOpts |= QRegularExpression::ExtendedPatternSyntaxOption; break;
case 'u': reOpts |= QRegularExpression::UseUnicodePropertiesOption; break;
default:
trace.setError(tr("Unexpected option '%1' after regex", "EventFilterModel").arg(c));
return NULL;
}
}
regex = QRegularExpression(param.mid(1, end - 1));
if(!regex.isValid())
{
trace.setError(tr("Invalid regex", "EventFilterModel"));
return NULL;
}
}
param = param.toLower();
comparer = [contains, param, regex](const SDObject *obj) {
if(obj->type.basetype == SDBasic::String)
{
if(contains)
{
QString str = obj->data.str;
str = str.toLower();
return str.contains(param);
}
else
{
QRegularExpressionMatch match = regex.match(QString(obj->data.str));
return match.isValid() && match.hasMatch();
}
}
return false;
};
}
else
{
trace.setError(tr("Unrecognised annotation condition", "EventFilterModel"));
return NULL;
}
}
return [keyPath, comparer](ICaptureContext *, const rdcstr &, const rdcstr &, uint32_t eventId,
const SDChunk *, const ActionDescription *action, const rdcstr &) {
if(action)
{
for(const APIEvent &e : action->events)
{
if(e.eventId == eventId)
{
const SDObject *annot = e.annotations ? e.annotations->FindChildByKeyPath(keyPath) : NULL;
if(annot == NULL)
return false;
if(comparer)
return comparer(annot);
else
return annot != NULL;
}
}
}
return false;
};
}
QString filterDescription_event() const
{
return tr(R"EOD(