mudlet/test/TMxpStubClient.h
Vadim Peretokin 4d36d19274
fix: MXP text now shows correctly on non-English games (#9491)
#### Brief overview of PR changes/additions

MXP element attribute values - such as `SEND` link targets/hints and
`<!ENTITY>` values - were always decoded as UTF-8, ignoring the
session's negotiated encoding. On non-UTF-8 games (Latin1, GBK, Big5,
WINDOWS-1251, ...) any non-ASCII bytes in an attribute were mis-decoded,
so MXP links and entities carried garbled text.

This threads the active session encoding into `TMxpNodeBuilder` (set by
`TMxpProcessor` before parsing each character) and decodes attribute
names/values through a new shared `TStringUtils::decodeBytes` helper.
That helper also replaces the duplicated decode logic previously inline
in `TMxpProcessor::decodeRawBytes`, so the attribute path and the raw
tag/content path now interpret bytes identically. An empty
(not-yet-negotiated) encoding continues to be treated as UTF-8,
preserving prior behaviour for default sessions.

#### Motivation for adding to Mudlet

Players on non-English MUDs saw corrupted text in MXP links and custom
entities whenever those carried accented or non-Latin characters - the
attribute parser was the only MXP path still hardcoding UTF-8 while
element content already honoured the session encoding.

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

Added regression tests:
- `TMxpEntityTagHandlerTest`: quoted and unquoted `<!ENTITY>` values in
a WINDOWS-1251 session decode to "Гроза"; a UTF-8 session still resolves
the same value unchanged.
- `TMxpSendTagHandlerTest`: a `SEND href` with Latin1 bytes decodes to
"señor"; an empty (default) session encoding still decodes attributes as
UTF-8.

The WINDOWS-1251 entity test was verified to fail on the baseline
(producing U+FFFD replacement characters) before the fix. All 14
MXP/encoding/entity unit tests pass.

Assisted-by: Claude:claude-opus-4-8
2026-07-27 19:55:26 +02:00

288 lines
7.3 KiB
C++

/***************************************************************************
* Copyright (C) 2020 by Gustavo Sousa - gustavocms@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. *
***************************************************************************/
#ifndef MUDLET_TEST_TMXPSTUBCLIENT_H
#define MUDLET_TEST_TMXPSTUBCLIENT_H
#include <qdebug.h>
#include "TMxpContext.h"
#include "TMxpClient.h"
#include "TMediaData.h"
class TMxpStubContext : public TMxpContext {
public:
TMxpElementRegistry mElementRegistry;
QMap<QString, QVector<QString>> mSupportedElements;
TEntityResolver mEntityResolver;
TMxpElementRegistry& getElementRegistry() override
{
return mElementRegistry;
}
QMap<QString, QVector<QString>>& getSupportedElements() override
{
return mSupportedElements;
}
TMxpTagHandlerResult handleTag(TMxpContext& ctx, TMxpClient& client, MxpTag* tag) override
{
return MXP_TAG_HANDLED;
}
void handleContent(char ch) override
{
}
TMxpTagHandler& getMainHandler() override
{
return *this;
}
TEntityResolver& getEntityResolver() override
{
return mEntityResolver;
}
};
class TMxpStubClient : public TMxpClient {
public:
QString version = "Stub-1.0";
bool linkMode;
QByteArray mEncoding = QByteArrayLiteral("UTF-8");
QByteArray getEncoding() const override { return mEncoding; }
QString sentToServer;
QString fgColor, bgColor;
QStringList mHrefs, mHints;
QString mExpireName;
QString mPublishedEntityName, mPublishedEntityValue;
QString style;
// Count how many of this format have been stacked/applied on top of each other
unsigned int boldCounter = 0;
unsigned int italicCounter = 0;
unsigned int underlineCounter = 0;
unsigned int strikeOutCounter = 0;
QString getVersion() override
{
return version;
}
void sendToServer(QString& str) override
{
sentToServer.append(str);
}
void setLinkMode(bool val) override
{
this->linkMode = val;
}
void setFlag(const QString& elementName, const QMap<QString, QString>& params, const QString& content) override
{
}
void pushColor(const QString& fgColor, const QString& bgColor) override
{
this->fgColor = fgColor;
this->bgColor = bgColor;
}
void popColor() override
{
fgColor.clear();
bgColor.clear();
}
void pushFont(const QString& fontFace, const QString& fontSize) override
{
}
void popFont() override
{
}
void setBold(bool bold) override
{
if (bold) {
boldCounter++;
} else if (boldCounter > 0) {
boldCounter--;
}
}
void setItalic(bool italic) override
{
if (italic) {
italicCounter++;
} else if (italicCounter > 0) {
italicCounter--;
}
}
void setUnderline(bool underline) override
{
if (underline) {
underlineCounter++;
} else if (underlineCounter > 0) {
underlineCounter--;
}
}
void setStrikeOut(bool strikeOut) override
{
if (strikeOut) {
strikeOutCounter++;
} else if (strikeOutCounter > 0) {
strikeOutCounter--;
}
}
bool bold() override
{
return boldCounter > 0;
}
bool italic() override
{
return italicCounter > 0;
}
bool underline() override
{
return underlineCounter > 0;
}
bool strikeOut() override
{
return strikeOutCounter > 0;
}
void resetTextProperties() override
{
}
void setStyle(const QString& val) override
{
style = val;
}
QString getStyle() override
{
return style;
}
int setLink(const QStringList& hrefs, const QStringList& hints) override
{
mHrefs = hrefs;
mHints = hints;
return 1;
}
int setLink(const QStringList& hrefs, const QStringList& hints, const QString& expireName) override
{
mHrefs = hrefs;
mHints = hints;
mExpireName = expireName;
return 1;
}
void expireLinks(const QString& expireName) override
{
qDebug().noquote() << qsl("expireLinks([%1])").arg(expireName);
}
bool getLink(int id, QStringList** href, QStringList** hint) override
{
*href = &mHrefs;
*hint = &mHints;
return true;
}
void playMedia(TMediaData& mediaData) override
{
}
void stopMedia(TMediaData& mediaData) override
{
}
void publishEntity(const QString& name, const QString& value) override
{
qDebug().noquote() << qsl("publishEntity([%1], [%2])").arg(name, value);
mPublishedEntityName = name;
mPublishedEntityValue = value;
}
void setVariable(const QString& name, const QString& value) override {}
// MXP Frame/Dest tracking
bool createMxpFrameCalled = false;
QString lastCreatedFrameName;
QMap<QString, QString> lastFrameAttributes;
bool setMxpDestinationCalled = false;
QString lastDestinationName;
bool lastDestinationEol = false;
bool lastDestinationEof = false;
bool clearMxpDestinationCalled = false;
bool createMxpFrame(const QString& name, const QMap<QString, QString>& attributes) override
{
createMxpFrameCalled = true;
lastCreatedFrameName = name;
lastFrameAttributes = attributes;
return false;
}
bool setMxpDestination(const QString& frameName, bool eol, bool eof) override
{
setMxpDestinationCalled = true;
lastDestinationName = frameName;
lastDestinationEol = eol;
lastDestinationEof = eof;
return false;
}
void clearMxpDestination() override
{
clearMxpDestinationCalled = true;
}
void resetMxpTracking()
{
createMxpFrameCalled = false;
lastCreatedFrameName.clear();
lastFrameAttributes.clear();
setMxpDestinationCalled = false;
lastDestinationName.clear();
lastDestinationEol = false;
lastDestinationEof = false;
clearMxpDestinationCalled = false;
}
};
#endif //MUDLET_TEST_TMXPSTUBCLIENT_H