mudlet/test/TMxpElementDefinitionHandlerTest.cpp
Mike Conley fa0db506c1
Fix: Text with < characters disappearing on games with MXP enabled (#8918)
#### Brief overview of PR changes/additions

Fixes text containing `<` characters being swallowed when connecting to
games that negotiate MXP via telnet but don't actually send MXP content.

The fix validates text against the MXP specification to distinguish real
MXP tags from regular text, preventing false positives when `<` appears
in normal game text.

#### Motivation for adding to Mudlet

Users reported that typing commands like `echo <test` resulted in no
output on certain games (e.g., Merentha). The `<` character was being
interpreted as the start of an MXP tag, causing text to disappear.

#### Other info (issues closed, discussion etc)

Fixes #8899, #8980
2026-03-21 17:45:49 +00:00

186 lines
7.5 KiB
C++

/***************************************************************************
* Copyright (C) 2025 by Nicolas Keita - nicolaskeita2@@gmail.com *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#include "TMxpElementRegistry.h"
#include "TMxpProcessor.h"
#include "TMxpStubClient.h"
#include <QString>
#include <QStringList>
#include <QtTest/QtTest>
class TmxpElementDefinitionHandlerTest : public QObject {
Q_OBJECT
private slots:
void testMxpElementRegistration_data() {
QTest::addColumn<QString>("name");
QTest::addColumn<QString>("inputString");
QTest::addColumn<QStringList>("expectedAttrs");
QTest::addColumn<QString>("expectedFlags");
QTest::addColumn<bool>("expectedOpen");
QTest::addColumn<bool>("expectedEmpty");
QTest::addColumn<int>("expectedParsedDefinitionCount");
QStringList attrs1;
QTest::newRow("Test1") << "Test1"
<< R"(<!ELEMENT Test1 FLAG='Weird & Co'>)" << attrs1
<< QString("Weird & Co") << false << false << 0;
QStringList attrs2 = {"name", "type"};
QTest::newRow("Test2")
<< "Test2"
<< R"(<!ELEMENT Test2 ATT="NAME=John TYPE=Admin" FLAG='SpecialFlag' OPEN EMPTY>)"
<< attrs2 << QString("SpecialFlag") << true << true << 0;
QStringList attrs3;
QTest::newRow("Test3")
<< "Test3"
<< R"(<!ELEMENT Test3 '<tag1 attr="val1"><tag2 attr="val2"><tag3 attr="val3">'>)"
<< attrs3 << QString() << false << false << 3;
QStringList attrs4 = {"class", "level", "race"};
QTest::newRow("Test4_MultipleAttrsAndFlag")
<< "Player"
<< R"(<!ELEMENT Player FLAG="Hero" ATT="CLASS=Warrior LEVEL=42 RACE=Elf">)"
<< attrs4 << QString("Hero") << false << false << 0;
QStringList attrs5;
QTest::newRow("Test5_EmptyFlag")
<< "EmptyFlag"
<< R"(<!ELEMENT EmptyFlag FLAG="">)" << attrs5 << QString("") << false
<< false << 0;
QStringList attrs6;
QTest::newRow("Test6_AutoClosingTag")
<< "AutoClose"
<< R"(<!ELEMENT AutoClose '<br/>' EMPTY>)" << attrs6 << QString()
<< false << true << 1;
QStringList attrs7;
QTest::newRow("Test7_NameWithDigitsAndUnderscore")
<< "Enemy_123"
<< R"(<!ELEMENT Enemy_123 FLAG="Dangerous">)" << attrs7
<< QString("Dangerous") << false << false << 0;
QStringList attrs8;
QTest::newRow("Test8_FlagWithChevronsAndAmpersand")
<< "Weird"
<< R"(<!ELEMENT Weird FLAG="Use <this> & that">)" << attrs8
<< QString("Use <this> & that") << false << false << 0;
}
void testMxpElementRegistration() {
QFETCH(QString, name);
QFETCH(QString, inputString);
QFETCH(QStringList, expectedAttrs);
QFETCH(QString, expectedFlags);
QFETCH(bool, expectedOpen);
QFETCH(bool, expectedEmpty);
QFETCH(int, expectedParsedDefinitionCount);
TMxpStubClient client;
TMxpProcessor mxpProcessor(&client);
mxpProcessor.setMode(MXP_MODE_CODE_LOCK_SECURE);
const QByteArray utf8Data = inputString.toUtf8();
for (char ch : utf8Data) {
mxpProcessor.processMxpInput(ch, true);
}
const TMxpElementRegistry &registry =
mxpProcessor.getMxpTagProcessor().getElementRegistry();
QVERIFY(registry.containsElement(name));
const TMxpElement &element = registry.getElement(name);
QCOMPARE(element.attrs, expectedAttrs);
QCOMPARE(element.flags, expectedFlags);
QCOMPARE(element.open, expectedOpen);
QCOMPARE(element.empty, expectedEmpty);
QCOMPARE(element.parsedDefinition.size(), expectedParsedDefinitionCount);
}
void testParserDoesNotFreezeOnAmpersand_data() {
QTest::addColumn<QString>("messageWithAmpersand");
QTest::newRow("no amp") << "abc";
QTest::newRow("solo amp") << "&\n";
QTest::newRow("amp followed by space") << "& followed by space\n";
QTest::newRow("amp at start no semicolon") << "&start\n";
QTest::newRow("amp + newline") << "line with &\nnext line\n";
QTest::newRow("amp + numbers") << "version &123;\n";
QTest::newRow("amp + special chars") << "weird &entity-._#;\n";
QTest::newRow("multiple consecutive amps") << "text &&&& more text\n";
QTest::newRow("only amps") << "&&&&\n";
QTest::newRow("amp + emoji") << "smile &😊;\n";
QTest::newRow("html_entity") << "Hello &lt;World&gt;\r";
QTest::newRow("known_custom_entity") << "Use &custom;\r";
QTest::newRow("incomplete_entity") << "Broken &ent\r";
QTest::newRow("empty_entity") << "Edge case &;\r";
QTest::newRow("long_entity")
<< "&thisisaverylongentitynamethatmightbreak;\r";
}
void testParserDoesNotFreezeOnAmpersand() {
TMxpStubClient stub;
TMxpProcessor mxpProcessor(&stub);
TMxpProcessingResult lastResult = HANDLER_FALL_THROUGH;
QFETCH(QString, messageWithAmpersand);
const QByteArray utf8Data = messageWithAmpersand.toUtf8();
for (char c : utf8Data) {
lastResult = mxpProcessor.processMxpInput(c, true);
}
QVERIFY(lastResult != HANDLER_NEXT_CHAR);
}
void testParserDoesNotFreezeOnUnescapedTagStart_data() {
QTest::addColumn<QString>("inputString");
QTest::newRow("starts_with_less_than") << "<test<!EN ob \"lamp\">";
QTest::newRow("less_than_second_char") << "a<test<!EN ob \"lamp\">";
QTest::newRow("multiple_less_than_before_valid_tag")
<< "<<<randomtext<!EN ob \"lamp\">";
QTest::newRow("unclosed_tag") << "<unclosed<!EN ob \"lamp\">";
QTest::newRow("closed_without_open") << "</ghost><!EN ob \"lamp\">";
QTest::newRow("mixed_with_text") << "foo<bar>baz<!EN ob \"lamp\">";
QTest::newRow("doctype_like") << "<!DOCTYPE fake><!EN ob \"lamp\">";
QTest::newRow("comment_tag") << "<!-- comment --><!EN ob \"lamp\">";
QTest::newRow("comment_with_less_than")
<< "<!-- this is a < comment -->\r<!EN ob \"lamp\">";
}
void testParserDoesNotFreezeOnUnescapedTagStart() {
TMxpStubClient stub;
TMxpProcessor mxpProcessor(&stub);
mxpProcessor.setMode(MXP_MODE_CODE_LOCK_SECURE);
QFETCH(QString, inputString);
const QByteArray utf8Data = inputString.toUtf8();
for (char c : utf8Data) {
mxpProcessor.processMxpInput(c, true);
}
QCOMPARE(
mxpProcessor.getMxpTagProcessor().getEntityResolver().getResolution(
"&ob;"),
"lamp");
}
};
#include "TMxpElementDefinitionHandlerTest.moc"
QTEST_MAIN(TmxpElementDefinitionHandlerTest)