Qt: Check field autocomplete for syntactical validity

Currently the autocompletion engine always suggests a protocol
field completion, even in places where it isn't syntactically
valid.

Fix that by compiling the preamble to the token under the cursor
and checking the returned error. If it is DF_ERROR_UNEXPECTED_END
that indicates a field or literal value was expected. Otherwise
a field replacement is not valid in this position.

Fixes #12811.
This commit is contained in:
João Valverde 2022-11-19 11:14:57 +00:00
parent 5853886d50
commit 967a3c3df9
11 changed files with 80 additions and 32 deletions

View File

@ -23,6 +23,8 @@ Wireshark is now better about generating valid UTF-8 output.
A new display filter feature for filtering raw bytes has been added.
Display filter autocomplete is smarter about not suggesting invalid syntax.
Many other improvements have been made.
See the “New and Updated Features” section below for more details.

View File

@ -53,6 +53,7 @@ dfilter_expand(const char *expr, char **err_ret);
*/
#define DF_ERROR_GENERIC -1
#define DF_ERROR_UNEXPECTED_END -2
typedef struct _dfilter_loc {
long col_start;

View File

@ -65,7 +65,7 @@ DIAG_ON_LEMON()
any "error" symbols are shifted, if possible. */
%syntax_error {
if (!TOKEN) {
dfilter_fail(dfw, DF_ERROR_GENERIC, NULL, "Unexpected end of filter expression.");
dfilter_fail(dfw, DF_ERROR_UNEXPECTED_END, NULL, "Unexpected end of filter expression.");
return;
}
FAIL(dfw, TOKEN, "\"%s\" was unexpected in this context.", stnode_token(TOKEN));

View File

@ -462,7 +462,7 @@ void CaptureFilterEdit::clearFilter()
emit textEdited(text());
}
void CaptureFilterEdit::buildCompletionList(const QString &primitive_word)
void CaptureFilterEdit::buildCompletionList(const QString &primitive_word, const QString &preamble _U_)
{
if (primitive_word.length() < 1) {
completion_model_->setStringList(QStringList());

View File

@ -66,7 +66,7 @@ private:
CaptureFilterSyntaxWorker *syntax_worker_;
QThread *syntax_thread_;
void buildCompletionList(const QString& primitive_word);
void buildCompletionList(const QString &primitive_word, const QString &preamble);
signals:
void captureFilterSyntaxChanged(bool valid);

View File

@ -71,7 +71,9 @@ DisplayFilterEdit::DisplayFilterEdit(QWidget *parent, DisplayFilterEditType type
clear_button_(NULL),
apply_button_(NULL),
leftAlignActions_(false),
last_applied_(QString())
last_applied_(QString()),
filter_word_preamble_(QString()),
autocomplete_accepts_field_(true)
{
setAccessibleName(tr("Display filter entry"));
@ -472,7 +474,7 @@ void DisplayFilterEdit::updateBookmarkMenu()
// - Recent and saved display filters in popup when editing first word.
// ui/gtk/filter_autocomplete.c:build_autocompletion_list
void DisplayFilterEdit::buildCompletionList(const QString &field_word)
void DisplayFilterEdit::buildCompletionList(const QString &field_word, const QString &preamble)
{
// Push a hint about the current field.
if (syntaxState() == Valid) {
@ -521,33 +523,57 @@ void DisplayFilterEdit::buildCompletionList(const QString &field_word)
completion_model_->setStringList(complex_list);
completer()->setCompletionPrefix(field_word);
void *proto_cookie;
// Only add fields to completion if a field is valid at this position.
// Try to compile preamble and check error message.
if (preamble != filter_word_preamble_) {
df_error_t *df_err = NULL;
dfilter_t *test_df = NULL;
if (preamble.size() > 0) {
dfilter_compile_real(qUtf8Printable(preamble), &test_df, &df_err,
DF_EXPAND_MACROS, __func__);
}
if (test_df == NULL || (df_err != NULL && df_err->code == DF_ERROR_UNEXPECTED_END)) {
// Unexpected end of expression means "expected identifier (field) or literal".
autocomplete_accepts_field_ = true;
}
else {
autocomplete_accepts_field_ = false;
}
dfilter_free(test_df);
dfilter_error_free(df_err);
filter_word_preamble_ = preamble;
}
QStringList field_list;
int field_dots = static_cast<int>(field_word.count('.')); // Some protocol names (_ws.expert) contain periods.
for (int proto_id = proto_get_first_protocol(&proto_cookie); proto_id != -1; proto_id = proto_get_next_protocol(&proto_cookie)) {
protocol_t *protocol = find_protocol_by_id(proto_id);
if (!proto_is_protocol_enabled(protocol)) continue;
if (autocomplete_accepts_field_) {
void *proto_cookie;
const QString pfname = proto_get_protocol_filter_name(proto_id);
field_list << pfname;
int field_dots = static_cast<int>(field_word.count('.')); // Some protocol names (_ws.expert) contain periods.
for (int proto_id = proto_get_first_protocol(&proto_cookie); proto_id != -1; proto_id = proto_get_next_protocol(&proto_cookie)) {
protocol_t *protocol = find_protocol_by_id(proto_id);
if (!proto_is_protocol_enabled(protocol)) continue;
// Add fields only if we're past the protocol name and only for the
// current protocol.
if (field_dots > pfname.count('.')) {
void *field_cookie;
const QByteArray fw_ba = field_word.toUtf8(); // or toLatin1 or toStdString?
const char *fw_utf8 = fw_ba.constData();
gsize fw_len = (gsize) strlen(fw_utf8);
for (header_field_info *hfinfo = proto_get_first_protocol_field(proto_id, &field_cookie); hfinfo; hfinfo = proto_get_next_protocol_field(proto_id, &field_cookie)) {
if (hfinfo->same_name_prev_id != -1) continue; // Ignore duplicate names.
const QString pfname = proto_get_protocol_filter_name(proto_id);
field_list << pfname;
if (!g_ascii_strncasecmp(fw_utf8, hfinfo->abbrev, fw_len)) {
if ((gsize) strlen(hfinfo->abbrev) != fw_len) field_list << hfinfo->abbrev;
// Add fields only if we're past the protocol name and only for the
// current protocol.
if (field_dots > pfname.count('.')) {
void *field_cookie;
const QByteArray fw_ba = field_word.toUtf8(); // or toLatin1 or toStdString?
const char *fw_utf8 = fw_ba.constData();
gsize fw_len = (gsize) strlen(fw_utf8);
for (header_field_info *hfinfo = proto_get_first_protocol_field(proto_id, &field_cookie); hfinfo; hfinfo = proto_get_next_protocol_field(proto_id, &field_cookie)) {
if (hfinfo->same_name_prev_id != -1) continue; // Ignore duplicate names.
if (!g_ascii_strncasecmp(fw_utf8, hfinfo->abbrev, fw_len)) {
if ((gsize) strlen(hfinfo->abbrev) != fw_len) field_list << hfinfo->abbrev;
}
}
}
}
field_list.sort();
}
field_list.sort();
completion_model_->setStringList(complex_list + field_list);
completer()->setCompletionPrefix(field_word);

View File

@ -76,9 +76,11 @@ private:
StockIconToolButton *apply_button_;
bool leftAlignActions_;
QString last_applied_;
QString filter_word_preamble_;
bool autocomplete_accepts_field_;
void setDefaultPlaceholderText();
void buildCompletionList(const QString& field_word);
void buildCompletionList(const QString &field_word, const QString &preamble);
void createFilterTextDropMenu(QDropEvent *event, bool prepare, QString filterText = QString());

View File

@ -125,7 +125,7 @@ void FieldFilterEdit::checkFilter(const QString& filter_text)
// - Recent and saved display filters in popup when editing first word.
// ui/gtk/filter_autocomplete.c:build_autocompletion_list
void FieldFilterEdit::buildCompletionList(const QString &field_word)
void FieldFilterEdit::buildCompletionList(const QString &field_word, const QString &preamble _U_)
{
// Push a hint about the current field.
if (syntaxState() == Valid) {

View File

@ -42,7 +42,7 @@ private:
QString placeholder_text_;
void setDefaultPlaceholderText();
void buildCompletionList(const QString& field_word);
void buildCompletionList(const QString &field_word, const QString &preamble);
signals:
void pushFilterSyntaxStatus(const QString&);

View File

@ -377,10 +377,9 @@ void SyntaxLineEdit::completionKeyPressEvent(QKeyEvent *event)
return;
}
QPoint token_coords(getTokenUnderCursor());
QString token_word = text().mid(token_coords.x(), token_coords.y());
buildCompletionList(token_word);
QStringList sentence(splitLineUnderCursor());
Q_ASSERT(sentence.size() == 2); // (preamble, token)
buildCompletionList(sentence[1] /* token */, sentence[0] /* preamble */);
if (completion_model_->stringList().length() < 1) {
completer_->popup()->hide();
@ -532,3 +531,19 @@ QPoint SyntaxLineEdit::getTokenUnderCursor()
return QPoint(start, len);
}
QStringList SyntaxLineEdit::splitLineUnderCursor()
{
QPoint token_coords(getTokenUnderCursor());
// Split line into preamble and word under cursor.
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
QString preamble = text().first(token_coords.x()).trimmed();
#else
QString preamble = text().mid(0, token_coords.x()).trimmed();
#endif
// This should be trimmed already
QString token_word = text().mid(token_coords.x(), token_coords.y());
return QStringList{ preamble, token_word };
}

View File

@ -60,9 +60,11 @@ protected:
QStringListModel *completion_model_;
void setCompletionTokenChars(const QString &token_chars) { token_chars_ = token_chars; }
bool isComplexFilter(const QString &filter);
virtual void buildCompletionList(const QString&) { }
virtual void buildCompletionList(const QString &field_word, const QString &preamble) { Q_UNUSED(field_word); Q_UNUSED(preamble); }
// x = Start position, y = length
QPoint getTokenUnderCursor();
// Returns (preamble, token)
QStringList splitLineUnderCursor();
virtual bool event(QEvent *event);
void completionKeyPressEvent(QKeyEvent *event);