mudlet/test/TEncodingHelperTest.cpp
Mike Conley 26b1f0f526
Fix: Restore Chinese and Korean text that showed up garbled (#9349)
#### Brief overview of PR changes/additions

Restores support for several East Asian text encodings (Big5,
Big5-HKSCS, GBK, GB18030 and EUC-KR) that stopped working in 4.21.0 on
some builds. Affected players saw garbled characters on screen and got a
"no codec found" error when trying to send text.

#### Motivation for adding to Mudlet

A change in 4.21.0 left these encodings unavailable on builds whose Qt
was not shipped with an extra text-conversion library, which broke
connections to many Chinese and Korean games (for example on macOS).
This change makes Mudlet fall back to a conversion that is always
available, so the encodings work everywhere again.

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

Closes #9344. Also adds automated tests so these encodings keep working
going forward.

<img width="1407" height="497" alt="Screenshot 2026-06-19 at 4 22 30 PM"
src="https://github.com/user-attachments/assets/8b4d172f-867d-4aa8-9e75-a0c0a4e56b3f"
/>

---------

Signed-off-by: Mike Conley (aka Tamarindo) <sousesider@gmail.com>
2026-06-20 12:32:47 +02:00

203 lines
8.8 KiB
C++

/***************************************************************************
* Copyright (C) 2026 by Mike Conley - mike.conley@stickmud.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 "TEncodingHelper.h"
#include <QtTest/QtTest>
/*
* Unit tests for TEncodingHelper.
*
* The class is a pure static utility (no GUI / Host / TMap dependencies), so
* these tests run without needing the full Mudlet application stack.
*
* Primary purpose: regression guard for issue #9344. The 4.21.0 encoding
* refactor moved Mudlet from Qt5's self-contained QTextCodec to Qt6's
* QStringConverter, which only offers the multibyte CJK encodings (Big5,
* Big5-HKSCS, GBK, GB18030, EUC-KR, ...) when Qt is built with ICU. On builds
* whose Qt lacks runtime ICU this silently broke CJK users (garbled display and
* "no codec found" send errors). TEncodingHelper now falls back to the
* Qt5Compat QTextCodec, whose CJK codec tables are ICU-independent.
*
* These tests assert the user-facing guarantee - that the CJK encodings remain
* available and round-trip correctly through TEncodingHelper - regardless of
* whether the underlying path is QStringConverter (ICU) or the Qt5Compat
* fallback. On a build lacking ICU, removing the fallback makes these tests
* fail, which is exactly the regression we want to catch.
*/
class TEncodingHelperTest : public QObject
{
Q_OBJECT
// Build a QString from BMP code points so the test does not depend on the
// source file's own text encoding.
static QString fromCodepoints(std::initializer_list<char16_t> codepoints)
{
QString result;
for (const char16_t cp : codepoints) {
result.append(QChar(cp));
}
return result;
}
private slots:
// -------------------------------------------------------------------------
// Availability - the core #9344 guarantee
// -------------------------------------------------------------------------
void isEncodingAvailable_utf8_true() { QVERIFY(TEncodingHelper::isEncodingAvailable("UTF-8")); }
void isEncodingAvailable_big5_true() { QVERIFY(TEncodingHelper::isEncodingAvailable("Big5")); }
void isEncodingAvailable_big5hkscs_true() { QVERIFY(TEncodingHelper::isEncodingAvailable("Big5-HKSCS")); }
void isEncodingAvailable_gbk_true() { QVERIFY(TEncodingHelper::isEncodingAvailable("GBK")); }
void isEncodingAvailable_gb18030_true() { QVERIFY(TEncodingHelper::isEncodingAvailable("GB18030")); }
void isEncodingAvailable_euckr_true() { QVERIFY(TEncodingHelper::isEncodingAvailable("EUC-KR")); }
void isEncodingAvailable_unknown_false() { QVERIFY(!TEncodingHelper::isEncodingAvailable("not-a-real-codec")); }
// -------------------------------------------------------------------------
// Big5 - the headline encoding from issue #9344, asserted against a known
// byte sequence so the test is meaningful even where ICU happens to work.
// "中文" -> U+4E2D U+6587 -> Big5 0xA4A4 0xA4E5
// -------------------------------------------------------------------------
void decode_big5_knownSequence()
{
const QByteArray bytes = QByteArray::fromHex("a4a4a4e5");
const QString expected = fromCodepoints({0x4E2D, 0x6587});
QCOMPARE(TEncodingHelper::decode(bytes, "Big5"), expected);
}
void encode_big5_knownSequence()
{
const QString input = fromCodepoints({0x4E2D, 0x6587});
const QByteArray expected = QByteArray::fromHex("a4a4a4e5");
QCOMPARE(TEncodingHelper::encode(input, "Big5"), expected);
}
void canEncode_big5_cjk_true()
{
const QString input = fromCodepoints({0x4E2D, 0x6587});
QVERIFY(TEncodingHelper::canEncode(input, "Big5"));
}
// Big5 (Traditional Chinese) cannot represent Hangul, so canEncode must
// report false - the branch behind the "no codec found" send-error in #9344.
void canEncode_big5_hangul_false()
{
const QString input = fromCodepoints({0xD55C, 0xAD6D}); // 한국
QVERIFY(!TEncodingHelper::canEncode(input, "Big5"));
}
void roundTrip_big5()
{
const QString input = fromCodepoints({0x4E2D, 0x6587, 0x0041}); // 中文A
const QByteArray encoded = TEncodingHelper::encode(input, "Big5");
QCOMPARE(TEncodingHelper::decode(encoded, "Big5"), input);
}
// -------------------------------------------------------------------------
// GBK / GB18030 - simplified Chinese. "中文" -> GBK 0xD6D0 0xCEC4
// -------------------------------------------------------------------------
void encode_gbk_knownSequence()
{
const QString input = fromCodepoints({0x4E2D, 0x6587});
const QByteArray expected = QByteArray::fromHex("d6d0cec4");
QCOMPARE(TEncodingHelper::encode(input, "GBK"), expected);
}
void roundTrip_gbk()
{
const QString input = fromCodepoints({0x4E2D, 0x6587, 0x0041});
const QByteArray encoded = TEncodingHelper::encode(input, "GBK");
QCOMPARE(TEncodingHelper::decode(encoded, "GBK"), input);
}
void roundTrip_gb18030()
{
const QString input = fromCodepoints({0x4E2D, 0x6587, 0x0041});
const QByteArray encoded = TEncodingHelper::encode(input, "GB18030");
QCOMPARE(TEncodingHelper::decode(encoded, "GB18030"), input);
}
// -------------------------------------------------------------------------
// EUC-KR - Korean. "한국" -> U+D55C U+AD6D
// -------------------------------------------------------------------------
void roundTrip_euckr()
{
const QString input = fromCodepoints({0xD55C, 0xAD6D, 0x0041}); // 한국A
const QByteArray encoded = TEncodingHelper::encode(input, "EUC-KR");
QCOMPARE(TEncodingHelper::decode(encoded, "EUC-KR"), input);
}
void canEncode_euckr_cjk_true()
{
const QString input = fromCodepoints({0xD55C, 0xAD6D});
QVERIFY(TEncodingHelper::canEncode(input, "EUC-KR"));
}
// -------------------------------------------------------------------------
// Unknown encoding - exercises the final fallthrough of each function once
// QStringConverter, the lookup tables and QTextCodec have all come up empty.
// -------------------------------------------------------------------------
void decode_unknown_fallsBackToLatin1()
{
const QByteArray bytes = QByteArrayLiteral("ABC");
QCOMPARE(TEncodingHelper::decode(bytes, "not-a-real-codec"), QStringLiteral("ABC"));
}
void encode_unknown_fallsBackToLatin1()
{
const QString input = QStringLiteral("ABC");
QCOMPARE(TEncodingHelper::encode(input, "not-a-real-codec"), QByteArrayLiteral("ABC"));
}
void canEncode_unknown_false() { QVERIFY(!TEncodingHelper::canEncode(QStringLiteral("ABC"), "not-a-real-codec")); }
// -------------------------------------------------------------------------
// Controls - encodings that never depended on the fallback, to ensure the
// fallback chain did not disturb the always-available paths.
// -------------------------------------------------------------------------
void roundTrip_utf8()
{
const QString input = fromCodepoints({0x4E2D, 0x6587, 0x0041});
const QByteArray encoded = TEncodingHelper::encode(input, "UTF-8");
QCOMPARE(TEncodingHelper::decode(encoded, "UTF-8"), input);
}
void roundTrip_latin1()
{
const QString input = fromCodepoints({0x0048, 0x00E9, 0x006C, 0x006C, 0x006F}); // Héllo
const QByteArray encoded = TEncodingHelper::encode(input, "ISO 8859-1");
QCOMPARE(TEncodingHelper::decode(encoded, "ISO 8859-1"), input);
}
};
QTEST_GUILESS_MAIN(TEncodingHelperTest)
#include "TEncodingHelperTest.moc"