mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
#### 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>
203 lines
8.8 KiB
C++
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"
|