From 6e9a3d420935ab67372edb8ef7bb7ca0b65517ac Mon Sep 17 00:00:00 2001 From: Ivan Solovev Date: Thu, 24 Feb 2022 15:43:23 +0100 Subject: [PATCH] QSettings: support reading UTF-8 keys in INI files [ChangeLog][QtCore][QSettings] The INI file reader now supports keys encoded with UTF-8, as well as the %-encoded format. Writing the keys back to the INI file is still done using %-encoded format. This change does not touch the way the *values* are handled - they are both read and written in UTF-8. Drive-by: remove misleading comments from the reading algorithm. Task-number: QTBUG-99401 Change-Id: I6a83cbf24d919a499540403688615f93cb195e93 Reviewed-by: Edward Welbourne --- src/corelib/io/qsettings.cpp | 32 +++++++++------- .../auto/corelib/io/qsettings/CMakeLists.txt | 2 + .../auto/corelib/io/qsettings/qt5settings.ini | 11 ++++++ .../corelib/io/qsettings/tst_qsettings.cpp | 38 +++++++++++++++++++ .../corelib/io/qsettings/utf8settings.ini | 11 ++++++ 5 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 tests/auto/corelib/io/qsettings/qt5settings.ini create mode 100644 tests/auto/corelib/io/qsettings/utf8settings.ini diff --git a/src/corelib/io/qsettings.cpp b/src/corelib/io/qsettings.cpp index 34f07096eb..fc31eca5de 100644 --- a/src/corelib/io/qsettings.cpp +++ b/src/corelib/io/qsettings.cpp @@ -545,11 +545,13 @@ void QSettingsPrivate::iniEscapedKey(const QString &key, QByteArray &result) bool QSettingsPrivate::iniUnescapedKey(const QByteArray &key, int from, int to, QString &result) { + const QString decoded = QString::fromUtf8(QByteArrayView(key).sliced(from, to - from)); + const qsizetype size = decoded.size(); + result.reserve(result.length() + size); + qsizetype i = 0; bool lowercaseOnly = true; - int i = from; - result.reserve(result.length() + (to - from)); - while (i < to) { - char16_t ch = (uchar)key.at(i); + while (i < size) { + char16_t ch = decoded.at(i).unicode(); if (ch == '\\') { result += QLatin1Char('/'); @@ -557,10 +559,11 @@ bool QSettingsPrivate::iniUnescapedKey(const QByteArray &key, int from, int to, continue; } - if (ch != '%' || i == to - 1) { - if (uint(ch - 'A') <= 'Z' - 'A') // only for ASCII + if (ch != '%' || i == size - 1) { + QChar qch(ch); + if (qch.isUpper()) lowercaseOnly = false; - result += ch; + result += qch; ++i; continue; } @@ -568,24 +571,22 @@ bool QSettingsPrivate::iniUnescapedKey(const QByteArray &key, int from, int to, int numDigits = 2; int firstDigitPos = i + 1; - ch = key.at(i + 1); + ch = decoded.at(i + 1).unicode(); if (ch == 'U') { ++firstDigitPos; numDigits = 4; } - if (firstDigitPos + numDigits > to) { + if (firstDigitPos + numDigits > size) { result += QLatin1Char('%'); - // ### missing U ++i; continue; } bool ok; - ch = key.mid(firstDigitPos, numDigits).toUShort(&ok, 16); + ch = QStringView(decoded).sliced(firstDigitPos, numDigits).toUShort(&ok, 16); if (!ok) { result += QLatin1Char('%'); - // ### missing U ++i; continue; } @@ -2427,9 +2428,12 @@ void QConfFileSettingsPrivate::ensureSectionParsed(QConfFile *confFile, such as "General/someKey", the key will be located in the "%General" section, \e not in the "General" section. - \li In line with most implementations today, QSettings will - assume the INI file is utf-8 encoded. This means that keys and values + \li In line with most implementations today, QSettings will assume that + \e values in the INI file are utf-8 encoded. This means that \e values will be decoded as utf-8 encoded entries and written back as utf-8. + To retain backward compatibility with older Qt versions, \e keys in the + INI file are written in %-encoded format, but can be read in both + %-encoded and utf-8 formats. \endlist diff --git a/tests/auto/corelib/io/qsettings/CMakeLists.txt b/tests/auto/corelib/io/qsettings/CMakeLists.txt index c9ba64f0ca..fa6af4eaf4 100644 --- a/tests/auto/corelib/io/qsettings/CMakeLists.txt +++ b/tests/auto/corelib/io/qsettings/CMakeLists.txt @@ -15,6 +15,8 @@ set(qsettings_resource_files "resourcefile6.plist" "withcomments.ini" "float.ini" + "qt5settings.ini" + "utf8settings.ini" ) qt_internal_add_test(tst_qsettings diff --git a/tests/auto/corelib/io/qsettings/qt5settings.ini b/tests/auto/corelib/io/qsettings/qt5settings.ini new file mode 100644 index 0000000000..59239d29f3 --- /dev/null +++ b/tests/auto/corelib/io/qsettings/qt5settings.ini @@ -0,0 +1,11 @@ +[General] +.%2C%27%25U%U0430%U0431%U0432%U0433%22%09=".,'%!@#$" +%U265F=\x2658\x265a +%UD83C%UDF0D=\xd83c\xdf10 + +[Test] +BAR=BAR +B%C4R=B\xc4R +OST=OST +%D6SE=\xd6SE +%U042D%U0442%U043E\%U0442%U0435%U0441%U0442=\x42d\x442\x43e \x442\x435\x441\x442 diff --git a/tests/auto/corelib/io/qsettings/tst_qsettings.cpp b/tests/auto/corelib/io/qsettings/tst_qsettings.cpp index 48087b6a65..820a879d63 100644 --- a/tests/auto/corelib/io/qsettings/tst_qsettings.cpp +++ b/tests/auto/corelib/io/qsettings/tst_qsettings.cpp @@ -198,6 +198,10 @@ private slots: void floatAsQVariant(); void testXdg(); + + void testReadKeys_data(); + void testReadKeys(); + private: void cleanupTestFiles(); @@ -3663,5 +3667,39 @@ void tst_QSettings::testXdg() #endif } +void tst_QSettings::testReadKeys_data() +{ + QTest::addColumn("filepath"); + + QTest::newRow("escaped") << ":/qt5settings.ini"; + QTest::newRow("utf-8") << ":/utf8settings.ini"; +} + +void tst_QSettings::testReadKeys() +{ + QFETCH(QString, filepath); + + QSettings settings(filepath, QSettings::IniFormat); + QCOMPARE(settings.status(), QSettings::NoError); + + QVariantMap expectedValues; + expectedValues.insert("Test/BAR", "BAR"); + expectedValues.insert("Test/OST", "OST"); + expectedValues.insert("Test/B\xC3\x84R", "B\xC3\x84R"); // BÄR + expectedValues.insert("Test/\xC3\x96SE", "\xC3\x96SE"); // ÖSE + expectedValues.insert( + "Test/\xD0\xAD\xD1\x82\xD0\xBE/\xD1\x82\xD0\xB5\xD1\x81\xD1\x82", // Это/тест + "\xD0\xAD\xD1\x82\xD0\xBE \xD1\x82\xD0\xB5\xD1\x81\xD1\x82"); // Это тест + expectedValues.insert(".,'%U\xD0\xB0\xD0\xB1\xD0\xB2\xD0\xB3\"\t", ".,'%!@#$"); + expectedValues.insert("\xE2\x99\x9F", "\xE2\x99\x98\xE2\x99\x9A"); // ♟︎ ♘♚ + expectedValues.insert("\xF0\x9F\x8C\x8D", "\xF0\x9F\x8C\x90"); // 🌍 🌐 + + QVariantMap readValues; + for (const auto &key : settings.allKeys()) + readValues.insert(key, settings.value(key)); + + QCOMPARE(readValues, expectedValues); +} + QTEST_MAIN(tst_QSettings) #include "tst_qsettings.moc" diff --git a/tests/auto/corelib/io/qsettings/utf8settings.ini b/tests/auto/corelib/io/qsettings/utf8settings.ini new file mode 100644 index 0000000000..bac010254f --- /dev/null +++ b/tests/auto/corelib/io/qsettings/utf8settings.ini @@ -0,0 +1,11 @@ +[General] +.%2C%27%25Uабвг%22%09=".,'%!@#$" +♟=♘♚ +🌍=🌐 + +[Test] +BAR=BAR +BÄR=BÄR +OST=OST +ÖSE=ÖSE +Это\тест=Это тест