diff --git a/autotests/kconfigtest.cpp b/autotests/kconfigtest.cpp index 736274c..4d38b15 100644 --- a/autotests/kconfigtest.cpp +++ b/autotests/kconfigtest.cpp @@ -1,1951 +1,1978 @@ /* This file is part of the KDE libraries SPDX-FileCopyrightText: 1997 Matthias Kalle Dalheimer SPDX-License-Identifier: LGPL-2.0-or-later */ // Qt5 TODO: re-enable. No point in doing it before, it breaks on QString::fromUtf8(QByteArray), which exists in qt5. #undef QT_NO_CAST_FROM_BYTEARRAY #include "kconfigtest.h" #include "helper.h" #include "config-kconfig.h" #include #include #include #include #include #include #include #ifdef Q_OS_UNIX #include #endif #ifndef Q_OS_WIN #include // getuid #endif KCONFIGGROUP_DECLARE_ENUM_QOBJECT(KConfigTest, Testing) KCONFIGGROUP_DECLARE_FLAGS_QOBJECT(KConfigTest, Flags) QTEST_MAIN(KConfigTest) Q_DECLARE_METATYPE(KConfigGroup) static QString homePath() { #ifdef Q_OS_WIN return QDir::homePath(); #else // Don't use QDir::homePath() on Unix, it removes any trailing slash, while KConfig uses $HOME. return QString::fromLocal8Bit(qgetenv("HOME")); #endif } #define BOOLENTRY1 true #define BOOLENTRY2 false #define STRINGENTRY1 "hello" #define STRINGENTRY2 " hello" #define STRINGENTRY3 "hello " #define STRINGENTRY4 " hello " #define STRINGENTRY5 " " #define STRINGENTRY6 "" #define UTF8BITENTRY "Hello äöü" #define TRANSLATEDSTRINGENTRY1 "bonjour" #define BYTEARRAYENTRY QByteArray( "\x00\xff\x7f\x3c abc\x00\x00", 10 ) #define ESCAPEKEY " []\0017[]==]" #define ESCAPEENTRY "[]\170[]]=3=]\\] " #define DOUBLEENTRY 123456.78912345 #define FLOATENTRY 123.567f #define POINTENTRY QPoint( 4351, 1235 ) #define SIZEENTRY QSize( 10, 20 ) #define RECTENTRY QRect( 10, 23, 5321, 13 ) #define DATETIMEENTRY QDateTime( QDate( 2002, 06, 23 ), QTime( 12, 55, 40 ) ) #define STRINGLISTENTRY (QStringList( "Hello," ) << " World") #define STRINGLISTEMPTYENTRY QStringList() #define STRINGLISTJUSTEMPTYELEMENT QStringList(QString()) #define STRINGLISTEMPTYTRAINLINGELEMENT (QStringList( "Hi" ) << QString()) #define STRINGLISTESCAPEODDENTRY (QStringList( "Hello\\\\\\" ) << "World") #define STRINGLISTESCAPEEVENENTRY (QStringList( "Hello\\\\\\\\" ) << "World") #define STRINGLISTESCAPECOMMAENTRY (QStringList( "Hel\\\\\\,\\\\,\\,\\\\\\\\,lo" ) << "World") #define INTLISTENTRY1 QList() << 1 << 2 << 3 << 4 #define BYTEARRAYLISTENTRY1 QList() << "" << "1,2" << "end" #define VARIANTLISTENTRY (QVariantList() << true << false << QString("joe") << 10023) #define VARIANTLISTENTRY2 (QVariantList() << POINTENTRY << SIZEENTRY) #define HOMEPATH QString(homePath()+"/foo") #define HOMEPATHESCAPE QString(homePath()+"/foo/$HOME") #define DOLLARGROUP "$i" #define TEST_SUBDIR "kconfigtest_subdir/" static inline QString testConfigDir() { return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/" TEST_SUBDIR; } static inline QString kdeGlobalsPath() { return QDir::cleanPath(testConfigDir() + "..") + "/kdeglobals"; } #ifndef Q_OS_WIN void initLocale() { setenv("LC_ALL", "en_US.utf-8", 1); setenv("TZ", "UTC", 1); } Q_CONSTRUCTOR_FUNCTION(initLocale) #endif void KConfigTest::initTestCase() { // ensure we don't use files in the real config directory QStandardPaths::setTestModeEnabled(true); qRegisterMetaType(); // to make sure all files from a previous failed run are deleted cleanupTestCase(); KSharedConfigPtr mainConfig = KSharedConfig::openConfig(); mainConfig->group("Main").writeEntry("Key", "Value"); mainConfig->sync(); KConfig sc(TEST_SUBDIR "kconfigtest"); KConfigGroup cg(&sc, "AAA"); cg.writeEntry("stringEntry1", STRINGENTRY1, KConfig::Persistent | KConfig::Global); cg.deleteEntry("stringEntry2", KConfig::Global); cg = KConfigGroup(&sc, "Hello"); cg.writeEntry("boolEntry1", BOOLENTRY1); cg.writeEntry("boolEntry2", BOOLENTRY2); QByteArray data(UTF8BITENTRY); QCOMPARE(data.size(), 12); // the source file is in utf8 QCOMPARE(QString::fromUtf8(data).length(), 9); cg.writeEntry("Test", data); cg.writeEntry("bytearrayEntry", BYTEARRAYENTRY); cg.writeEntry(ESCAPEKEY, ESCAPEENTRY); cg.writeEntry("emptyEntry", ""); cg.writeEntry("stringEntry1", STRINGENTRY1); cg.writeEntry("stringEntry2", STRINGENTRY2); cg.writeEntry("stringEntry3", STRINGENTRY3); cg.writeEntry("stringEntry4", STRINGENTRY4); cg.writeEntry("stringEntry5", STRINGENTRY5); cg.writeEntry("urlEntry1", QUrl(QStringLiteral("http://qt-project.org"))); cg.writeEntry("keywith=equalsign", STRINGENTRY1); cg.deleteEntry("stringEntry5"); cg.deleteEntry("stringEntry6"); // deleting a nonexistent entry cg.writeEntry("byteArrayEntry1", QByteArray(STRINGENTRY1), KConfig::Global | KConfig::Persistent); cg.writeEntry("doubleEntry1", DOUBLEENTRY); cg.writeEntry("floatEntry1", FLOATENTRY); sc.deleteGroup("deleteMe"); // deleting a nonexistent group cg = KConfigGroup(&sc, "Complex Types"); cg.writeEntry("rectEntry", RECTENTRY); cg.writeEntry("pointEntry", POINTENTRY); cg.writeEntry("sizeEntry", SIZEENTRY); cg.writeEntry("dateTimeEntry", DATETIMEENTRY); cg.writeEntry("dateEntry", DATETIMEENTRY.date()); KConfigGroup ct = cg; cg = KConfigGroup(&ct, "Nested Group 1"); cg.writeEntry("stringentry1", STRINGENTRY1); cg = KConfigGroup(&ct, "Nested Group 2"); cg.writeEntry("stringEntry2", STRINGENTRY2); cg = KConfigGroup(&cg, "Nested Group 2.1"); cg.writeEntry("stringEntry3", STRINGENTRY3); cg = KConfigGroup(&ct, "Nested Group 3"); cg.writeEntry("stringEntry3", STRINGENTRY3); cg = KConfigGroup(&sc, "List Types"); cg.writeEntry("listOfIntsEntry1", INTLISTENTRY1); cg.writeEntry("listOfByteArraysEntry1", BYTEARRAYLISTENTRY1); cg.writeEntry("stringListEntry", STRINGLISTENTRY); cg.writeEntry("stringListEmptyEntry", STRINGLISTEMPTYENTRY); cg.writeEntry("stringListJustEmptyElement", STRINGLISTJUSTEMPTYELEMENT); cg.writeEntry("stringListEmptyTrailingElement", STRINGLISTEMPTYTRAINLINGELEMENT); cg.writeEntry("stringListEscapeOddEntry", STRINGLISTESCAPEODDENTRY); cg.writeEntry("stringListEscapeEvenEntry", STRINGLISTESCAPEEVENENTRY); cg.writeEntry("stringListEscapeCommaEntry", STRINGLISTESCAPECOMMAENTRY); cg.writeEntry("variantListEntry", VARIANTLISTENTRY); cg = KConfigGroup(&sc, "Path Type"); cg.writePathEntry("homepath", HOMEPATH); cg.writePathEntry("homepathescape", HOMEPATHESCAPE); cg = KConfigGroup(&sc, "Enum Types"); #if defined(_MSC_VER) && _MSC_VER == 1600 cg.writeEntry("dummy", 42); #else //Visual C++ 2010 throws an Internal Compiler Error here cg.writeEntry("enum-10", Tens); cg.writeEntry("enum-100", Hundreds); cg.writeEntry("flags-bit0", Flags(bit0)); cg.writeEntry("flags-bit0-bit1", Flags(bit0 | bit1)); #endif cg = KConfigGroup(&sc, "ParentGroup"); KConfigGroup cg1(&cg, "SubGroup1"); cg1.writeEntry("somestring", "somevalue"); cg.writeEntry("parentgrpstring", "somevalue"); KConfigGroup cg2(&cg, "SubGroup2"); cg2.writeEntry("substring", "somevalue"); KConfigGroup cg3(&cg, "SubGroup/3"); cg3.writeEntry("sub3string", "somevalue"); QVERIFY(sc.isDirty()); QVERIFY(sc.sync()); QVERIFY(!sc.isDirty()); QVERIFY2(QFile::exists(testConfigDir() + QStringLiteral("/kconfigtest")), qPrintable(testConfigDir() + QStringLiteral("/kconfigtest must exist"))); QVERIFY2(QFile::exists(kdeGlobalsPath()), qPrintable(kdeGlobalsPath() + QStringLiteral(" must exist"))); KConfig sc1(TEST_SUBDIR "kdebugrc", KConfig::SimpleConfig); KConfigGroup sg0(&sc1, "0"); sg0.writeEntry("AbortFatal", false); sg0.writeEntry("WarnOutput", 0); sg0.writeEntry("FatalOutput", 0); QVERIFY(sc1.sync()); //Setup stuff to test KConfig::addConfigSources() KConfig devcfg(TEST_SUBDIR "specificrc"); KConfigGroup devonlygrp(&devcfg, "Specific Only Group"); devonlygrp.writeEntry("ExistingEntry", "DevValue"); KConfigGroup devandbasegrp(&devcfg, "Shared Group"); devandbasegrp.writeEntry("SomeSharedEntry", "DevValue"); devandbasegrp.writeEntry("SomeSpecificOnlyEntry", "DevValue"); QVERIFY(devcfg.sync()); KConfig basecfg(TEST_SUBDIR "baserc"); KConfigGroup basegrp(&basecfg, "Base Only Group"); basegrp.writeEntry("ExistingEntry", "BaseValue"); KConfigGroup baseanddevgrp(&basecfg, "Shared Group"); baseanddevgrp.writeEntry("SomeSharedEntry", "BaseValue"); baseanddevgrp.writeEntry("SomeBaseOnlyEntry", "BaseValue"); QVERIFY(basecfg.sync()); KConfig gecfg(TEST_SUBDIR "groupescapetest", KConfig::SimpleConfig); cg = KConfigGroup(&gecfg, DOLLARGROUP); cg.writeEntry("entry", "doesntmatter"); } void KConfigTest::cleanupTestCase() { //ensure we don't delete the real directory QDir localConfig(testConfigDir()); //qDebug() << "Erasing" << localConfig; if (localConfig.exists()) { QVERIFY(localConfig.removeRecursively()); } QVERIFY(!localConfig.exists()); if (QFile::exists(kdeGlobalsPath())) { QVERIFY(QFile::remove(kdeGlobalsPath())); } } static QList readLinesFrom(const QString &path) { QFile file(path); const bool opened = file.open(QIODevice::ReadOnly | QIODevice::Text); QList lines; if (!opened) { QWARN(qPrintable(QLatin1String("Failed to open ") + path)); return lines; } QByteArray line; do { line = file.readLine(); if (!line.isEmpty()) { lines.append(line); } } while (!line.isEmpty()); return lines; } static QList readLines(const char *fileName = TEST_SUBDIR "kconfigtest") { const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); Q_ASSERT(!path.isEmpty()); return readLinesFrom(path + '/' + fileName); } // see also testDefaults, which tests reverting with a defaults (global) file available void KConfigTest::testDirtyAfterRevert() { KConfig sc(TEST_SUBDIR "kconfigtest_revert"); KConfigGroup cg(&sc, "Hello"); cg.revertToDefault("does_not_exist"); QVERIFY(!sc.isDirty()); cg.writeEntry("Test", "Correct"); QVERIFY(sc.isDirty()); sc.sync(); QVERIFY(!sc.isDirty()); cg.revertToDefault("Test"); QVERIFY(sc.isDirty()); QVERIFY(sc.sync()); QVERIFY(!sc.isDirty()); cg.revertToDefault("Test"); QVERIFY(!sc.isDirty()); } void KConfigTest::testRevertAllEntries() { // this tests the case were we revert (delete) all entries in a file, // leaving a blank file { KConfig sc(TEST_SUBDIR "konfigtest2", KConfig::SimpleConfig); KConfigGroup cg(&sc, "Hello"); cg.writeEntry("Test", "Correct"); } { KConfig sc(TEST_SUBDIR "konfigtest2", KConfig::SimpleConfig); KConfigGroup cg(&sc, "Hello"); QCOMPARE(cg.readEntry("Test", "Default"), QString("Correct")); cg.revertToDefault("Test"); } KConfig sc(TEST_SUBDIR "konfigtest2", KConfig::SimpleConfig); KConfigGroup cg(&sc, "Hello"); QCOMPARE(cg.readEntry("Test", "Default"), QString("Default")); } void KConfigTest::testSimple() { // kdeglobals (which was created in initTestCase) must be found this way: const QStringList kdeglobals = QStandardPaths::locateAll(QStandardPaths::GenericConfigLocation, QStringLiteral("kdeglobals")); QVERIFY(!kdeglobals.isEmpty()); KConfig sc2(TEST_SUBDIR "kconfigtest"); QCOMPARE(sc2.name(), QString(TEST_SUBDIR "kconfigtest")); // make sure groupList() isn't returning something it shouldn't const QStringList lstGroup = sc2.groupList(); for (const QString &group : lstGroup) { QVERIFY(!group.isEmpty() && group != ""); QVERIFY(!group.contains(QChar(0x1d))); } KConfigGroup sc3(&sc2, "AAA"); QVERIFY(sc3.hasKey("stringEntry1")); // from kdeglobals QVERIFY(!sc3.isEntryImmutable("stringEntry1")); QCOMPARE(sc3.readEntry("stringEntry1"), QString(STRINGENTRY1)); QVERIFY(!sc3.hasKey("stringEntry2")); QCOMPARE(sc3.readEntry("stringEntry2", QString("bla")), QString("bla")); QVERIFY(!sc3.hasDefault("stringEntry1")); sc3 = KConfigGroup(&sc2, "Hello"); QCOMPARE(sc3.readEntry("Test", QByteArray()), QByteArray(UTF8BITENTRY)); QCOMPARE(sc3.readEntry("bytearrayEntry", QByteArray()), BYTEARRAYENTRY); QCOMPARE(sc3.readEntry(ESCAPEKEY), QString(ESCAPEENTRY)); QCOMPARE(sc3.readEntry("Test", QString()), QString::fromUtf8(UTF8BITENTRY)); QCOMPARE(sc3.readEntry("emptyEntry"/*, QString("Fietsbel")*/), QLatin1String("")); QCOMPARE(sc3.readEntry("emptyEntry", QString("Fietsbel")).isEmpty(), true); QCOMPARE(sc3.readEntry("stringEntry1"), QString(STRINGENTRY1)); QCOMPARE(sc3.readEntry("stringEntry2"), QString(STRINGENTRY2)); QCOMPARE(sc3.readEntry("stringEntry3"), QString(STRINGENTRY3)); QCOMPARE(sc3.readEntry("stringEntry4"), QString(STRINGENTRY4)); QVERIFY(!sc3.hasKey("stringEntry5")); QCOMPARE(sc3.readEntry("stringEntry5", QString("test")), QString("test")); QVERIFY(!sc3.hasKey("stringEntry6")); QCOMPARE(sc3.readEntry("stringEntry6", QString("foo")), QString("foo")); QCOMPARE(sc3.readEntry("urlEntry1", QUrl()), QUrl("http://qt-project.org")); QCOMPARE(sc3.readEntry("boolEntry1", BOOLENTRY1), BOOLENTRY1); QCOMPARE(sc3.readEntry("boolEntry2", false), BOOLENTRY2); QCOMPARE(sc3.readEntry("keywith=equalsign", QString("wrong")), QString(STRINGENTRY1)); QCOMPARE(sc3.readEntry("byteArrayEntry1", QByteArray()), QByteArray(STRINGENTRY1)); QCOMPARE(sc3.readEntry("doubleEntry1", 0.0), DOUBLEENTRY); QCOMPARE(sc3.readEntry("floatEntry1", 0.0f), FLOATENTRY); } void KConfigTest::testDefaults() { KConfig config(TEST_SUBDIR "defaulttest", KConfig::NoGlobals); const QString defaultsFile = TEST_SUBDIR "defaulttest.defaults"; KConfig defaults(defaultsFile, KConfig::SimpleConfig); const QString Default(QStringLiteral("Default")); const QString NotDefault(QStringLiteral("Not Default")); const QString Value1(STRINGENTRY1); const QString Value2(STRINGENTRY2); KConfigGroup group = defaults.group("any group"); group.writeEntry("entry1", Default); QVERIFY(group.sync()); group = config.group("any group"); group.writeEntry("entry1", Value1); group.writeEntry("entry2", Value2); QVERIFY(group.sync()); config.addConfigSources(QStringList() << QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + '/' + defaultsFile); config.setReadDefaults(true); QCOMPARE(group.readEntry("entry1", QString()), Default); QCOMPARE(group.readEntry("entry2", NotDefault), NotDefault); // no default for entry2 config.setReadDefaults(false); QCOMPARE(group.readEntry("entry1", Default), Value1); QCOMPARE(group.readEntry("entry2", NotDefault), Value2); group.revertToDefault("entry1"); QCOMPARE(group.readEntry("entry1", QString()), Default); group.revertToDefault("entry2"); QCOMPARE(group.readEntry("entry2", QString()), QString()); // TODO test reverting localized entries Q_ASSERT(config.isDirty()); group.sync(); // Check that everything is OK on disk, too KConfig reader(TEST_SUBDIR "defaulttest", KConfig::NoGlobals); reader.addConfigSources(QStringList() << QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + '/' + defaultsFile); KConfigGroup readerGroup = reader.group("any group"); QCOMPARE(readerGroup.readEntry("entry1", QString()), Default); QCOMPARE(readerGroup.readEntry("entry2", QString()), QString()); } void KConfigTest::testLocale() { KConfig config(TEST_SUBDIR "kconfigtest.locales", KConfig::SimpleConfig); const QString Translated(TRANSLATEDSTRINGENTRY1); const QString Untranslated(STRINGENTRY1); KConfigGroup group = config.group("Hello"); group.writeEntry("stringEntry1", Untranslated); config.setLocale(QStringLiteral("fr")); group.writeEntry("stringEntry1", Translated, KConfig::Localized | KConfig::Persistent); QVERIFY(config.sync()); QCOMPARE(group.readEntry("stringEntry1", QString()), Translated); QCOMPARE(group.readEntryUntranslated("stringEntry1"), Untranslated); config.setLocale(QStringLiteral("C")); // strings written in the "C" locale are written as nonlocalized group.writeEntry("stringEntry1", Untranslated, KConfig::Localized | KConfig::Persistent); QVERIFY(config.sync()); QCOMPARE(group.readEntry("stringEntry1", QString()), Untranslated); } void KConfigTest::testEncoding() { QString groupstr = QString::fromUtf8("UTF-8:\xc3\xb6l"); KConfig c(TEST_SUBDIR "kconfigtestencodings"); KConfigGroup cg(&c, groupstr); cg.writeEntry("key", "value"); QVERIFY(c.sync()); QList lines = readLines(TEST_SUBDIR "kconfigtestencodings"); QCOMPARE(lines.count(), 2); QCOMPARE(lines.first(), QByteArray("[UTF-8:\xc3\xb6l]\n")); KConfig c2(TEST_SUBDIR "kconfigtestencodings"); KConfigGroup cg2(&c2, groupstr); QVERIFY(cg2.readEntry("key") == QByteArray("value")); QVERIFY(c2.groupList().contains(groupstr)); } void KConfigTest::testLists() { KConfig sc2(TEST_SUBDIR "kconfigtest"); KConfigGroup sc3(&sc2, "List Types"); QCOMPARE(sc3.readEntry(QString("stringListEntry"), QStringList()), STRINGLISTENTRY); QCOMPARE(sc3.readEntry(QString("stringListEmptyEntry"), QStringList("wrong")), STRINGLISTEMPTYENTRY); QCOMPARE(sc3.readEntry(QString("stringListJustEmptyElement"), QStringList()), STRINGLISTJUSTEMPTYELEMENT); QCOMPARE(sc3.readEntry(QString("stringListEmptyTrailingElement"), QStringList()), STRINGLISTEMPTYTRAINLINGELEMENT); QCOMPARE(sc3.readEntry(QString("stringListEscapeOddEntry"), QStringList()), STRINGLISTESCAPEODDENTRY); QCOMPARE(sc3.readEntry(QString("stringListEscapeEvenEntry"), QStringList()), STRINGLISTESCAPEEVENENTRY); QCOMPARE(sc3.readEntry(QString("stringListEscapeCommaEntry"), QStringList()), STRINGLISTESCAPECOMMAENTRY); QCOMPARE(sc3.readEntry("listOfIntsEntry1"), QString::fromLatin1("1,2,3,4")); QList expectedIntList = INTLISTENTRY1; QVERIFY(sc3.readEntry("listOfIntsEntry1", QList()) == expectedIntList); QCOMPARE(QVariant(sc3.readEntry("variantListEntry", VARIANTLISTENTRY)).toStringList(), QVariant(VARIANTLISTENTRY).toStringList()); QCOMPARE(sc3.readEntry("listOfByteArraysEntry1", QList()), BYTEARRAYLISTENTRY1); } void KConfigTest::testPath() { KConfig sc2(TEST_SUBDIR "kconfigtest"); KConfigGroup sc3(&sc2, "Path Type"); QCOMPARE(sc3.readPathEntry("homepath", QString()), HOMEPATH); QCOMPARE(sc3.readPathEntry("homepathescape", QString()), HOMEPATHESCAPE); QCOMPARE(sc3.entryMap()["homepath"], HOMEPATH); qputenv("WITHSLASH", "/a/"); { QFile file(testConfigDir() + "/pathtest"); file.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream out(&file); out.setCodec("UTF-8"); out << "[Test Group]\n" << "homePath=$HOME/foo\n" << "homePath2=file://$HOME/foo\n" << "withSlash=$WITHSLASH/foo\n" << "withSlash2=$WITHSLASH\n" << "withBraces[$e]=file://${HOME}/foo\n" << "URL[$e]=file://${HOME}/foo\n" << "hostname[$e]=$(hostname)\n" << "escapes=aaa,bb/b,ccc\\,ccc\n" << "noeol=foo" // no EOL ; } KConfig cf2(TEST_SUBDIR "pathtest"); KConfigGroup group = cf2.group("Test Group"); QVERIFY(group.hasKey("homePath")); QCOMPARE(group.readPathEntry("homePath", QString()), HOMEPATH); QVERIFY(group.hasKey("homePath2")); QCOMPARE(group.readPathEntry("homePath2", QString()), QString("file://" + HOMEPATH)); QVERIFY(group.hasKey("withSlash")); QCOMPARE(group.readPathEntry("withSlash", QString()), QStringLiteral("/a//foo")); QVERIFY(group.hasKey("withSlash2")); QCOMPARE(group.readPathEntry("withSlash2", QString()), QStringLiteral("/a/")); QVERIFY(group.hasKey("withBraces")); QCOMPARE(group.readPathEntry("withBraces", QString()), QString("file://" + HOMEPATH)); QVERIFY(group.hasKey("URL")); QCOMPARE(group.readEntry("URL", QString()), QString("file://" + HOMEPATH)); QVERIFY(group.hasKey("hostname")); QCOMPARE(group.readEntry("hostname", QString()), QStringLiteral("(hostname)")); // the $ got removed because empty var name QVERIFY(group.hasKey("noeol")); QCOMPARE(group.readEntry("noeol", QString()), QString("foo")); const auto val = QStringList { QStringLiteral("aaa"), QStringLiteral("bb/b"), QStringLiteral("ccc,ccc")}; QCOMPARE(group.readPathEntry("escapes", QStringList()), val); } void KConfigTest::testPersistenceOfExpandFlagForPath() { // This test checks that a path entry starting with $HOME is still flagged // with the expand flag after the config was altered without rewriting the // path entry. // 1st step: Open the config, add a new dummy entry and then sync the config // back to the storage. { KConfig sc2(TEST_SUBDIR "kconfigtest"); KConfigGroup sc3(&sc2, "Path Type"); sc3.writeEntry("dummy", "dummy"); QVERIFY(sc2.sync()); } // 2nd step: Call testPath() again. Rewriting the config must not break // the testPath() test. testPath(); } void KConfigTest::testPathQtHome() { { QFile file(testConfigDir() + "/pathtest"); file.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream out(&file); out.setCodec("UTF-8"); out << "[Test Group]\n" << "dataDir[$e]=$QT_DATA_HOME/kconfigtest\n" << "cacheDir[$e]=$QT_CACHE_HOME/kconfigtest\n" << "configDir[$e]=$QT_CONFIG_HOME/kconfigtest\n"; } KConfig cf2(TEST_SUBDIR "pathtest"); KConfigGroup group = cf2.group("Test Group"); qunsetenv("QT_DATA_HOME"); qunsetenv("QT_CACHE_HOME"); qunsetenv("QT_CONFIG_HOME"); QVERIFY(group.hasKey("dataDir")); QCOMPARE(group.readEntry("dataDir", QString()), QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation).append(QStringLiteral("/kconfigtest"))); QVERIFY(group.hasKey("cacheDir")); QCOMPARE(group.readEntry("cacheDir", QString()), QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation).append(QStringLiteral("/kconfigtest"))); QVERIFY(group.hasKey("configDir")); QCOMPARE(group.readEntry("configDir", QString()), QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation).append(QStringLiteral("/kconfigtest"))); qputenv("QT_DATA_HOME","/1"); qputenv("QT_CACHE_HOME","/2"); qputenv("QT_CONFIG_HOME","/3"); QVERIFY(group.hasKey("dataDir")); QCOMPARE(group.readEntry("dataDir", QString()), QStringLiteral("/1/kconfigtest")); QVERIFY(group.hasKey("cacheDir")); QCOMPARE(group.readEntry("cacheDir", QString()), QStringLiteral("/2/kconfigtest")); QVERIFY(group.hasKey("configDir")); QCOMPARE(group.readEntry("configDir", QString()), QStringLiteral("/3/kconfigtest")); } void KConfigTest::testComplex() { KConfig sc2(TEST_SUBDIR "kconfigtest"); KConfigGroup sc3(&sc2, "Complex Types"); QCOMPARE(sc3.readEntry("pointEntry", QPoint()), POINTENTRY); QCOMPARE(sc3.readEntry("sizeEntry", SIZEENTRY), SIZEENTRY); QCOMPARE(sc3.readEntry("rectEntry", QRect(1, 2, 3, 4)), RECTENTRY); QCOMPARE(sc3.readEntry("dateTimeEntry", QDateTime()).toString(Qt::ISODate), DATETIMEENTRY.toString(Qt::ISODate)); QCOMPARE(sc3.readEntry("dateEntry", QDate()).toString(Qt::ISODate), DATETIMEENTRY.date().toString(Qt::ISODate)); QCOMPARE(sc3.readEntry("dateTimeEntry", QDate()), DATETIMEENTRY.date()); } void KConfigTest::testEnums() { //Visual C++ 2010 (compiler version 16.0) throws an Internal Compiler Error //when compiling the code in initTestCase that creates these KConfig entries, //so we can't run this test #if defined(_MSC_VER) && _MSC_VER == 1600 QSKIP("Visual C++ 2010 can't compile this test"); #endif KConfig sc(TEST_SUBDIR "kconfigtest"); KConfigGroup sc3(&sc, "Enum Types"); QCOMPARE(sc3.readEntry("enum-10"), QString("Tens")); QVERIFY(sc3.readEntry("enum-100", Ones) != Ones); QVERIFY(sc3.readEntry("enum-100", Ones) != Tens); QCOMPARE(sc3.readEntry("flags-bit0"), QString("bit0")); QVERIFY(sc3.readEntry("flags-bit0", Flags()) == bit0); int eid = staticMetaObject.indexOfEnumerator("Flags"); QVERIFY(eid != -1); QMetaEnum me = staticMetaObject.enumerator(eid); Flags bitfield = bit0 | bit1; QCOMPARE(sc3.readEntry("flags-bit0-bit1"), QString(me.valueToKeys(bitfield))); QVERIFY(sc3.readEntry("flags-bit0-bit1", Flags()) == bitfield); } void KConfigTest::testEntryMap() { KConfig sc(TEST_SUBDIR "kconfigtest"); KConfigGroup cg(&sc, "Hello"); QMap entryMap = cg.entryMap(); qDebug() << entryMap.keys(); QCOMPARE(entryMap.value("stringEntry1"), QString(STRINGENTRY1)); QCOMPARE(entryMap.value("stringEntry2"), QString(STRINGENTRY2)); QCOMPARE(entryMap.value("stringEntry3"), QString(STRINGENTRY3)); QCOMPARE(entryMap.value("stringEntry4"), QString(STRINGENTRY4)); QVERIFY(!entryMap.contains("stringEntry5")); QVERIFY(!entryMap.contains("stringEntry6")); QCOMPARE(entryMap.value("Test"), QString::fromUtf8(UTF8BITENTRY)); QCOMPARE(entryMap.value("bytearrayEntry"), QString::fromUtf8(BYTEARRAYENTRY.constData())); QCOMPARE(entryMap.value("emptyEntry"), QString()); QVERIFY(entryMap.contains("emptyEntry")); QCOMPARE(entryMap.value("boolEntry1"), QString(BOOLENTRY1 ? "true" : "false")); QCOMPARE(entryMap.value("boolEntry2"), QString(BOOLENTRY2 ? "true" : "false")); QCOMPARE(entryMap.value("keywith=equalsign"), QString(STRINGENTRY1)); QCOMPARE(entryMap.value("byteArrayEntry1"), QString(STRINGENTRY1)); QCOMPARE(entryMap.value("doubleEntry1").toDouble(), DOUBLEENTRY); QCOMPARE(entryMap.value("floatEntry1").toFloat(), FLOATENTRY); } void KConfigTest::testInvalid() { KConfig sc(TEST_SUBDIR "kconfigtest"); // all of these should print a message to the kdebug.dbg file KConfigGroup sc3(&sc, "Invalid Types"); sc3.writeEntry("badList", VARIANTLISTENTRY2); QList list; // 1 element list list << 1; sc3.writeEntry(QStringLiteral("badList"), list); QVERIFY(sc3.readEntry("badList", QPoint()) == QPoint()); QVERIFY(sc3.readEntry("badList", QRect()) == QRect()); QVERIFY(sc3.readEntry("badList", QSize()) == QSize()); QVERIFY(sc3.readEntry("badList", QDate()) == QDate()); QVERIFY(sc3.readEntry("badList", QDateTime()) == QDateTime()); // 2 element list list << 2; sc3.writeEntry("badList", list); QVERIFY(sc3.readEntry("badList", QRect()) == QRect()); QVERIFY(sc3.readEntry("badList", QDate()) == QDate()); QVERIFY(sc3.readEntry("badList", QDateTime()) == QDateTime()); // 3 element list list << 303; sc3.writeEntry("badList", list); QVERIFY(sc3.readEntry("badList", QPoint()) == QPoint()); QVERIFY(sc3.readEntry("badList", QRect()) == QRect()); QVERIFY(sc3.readEntry("badList", QSize()) == QSize()); QVERIFY(sc3.readEntry("badList", QDate()) == QDate()); // out of bounds QVERIFY(sc3.readEntry("badList", QDateTime()) == QDateTime()); // 4 element list list << 4; sc3.writeEntry("badList", list); QVERIFY(sc3.readEntry("badList", QPoint()) == QPoint()); QVERIFY(sc3.readEntry("badList", QSize()) == QSize()); QVERIFY(sc3.readEntry("badList", QDate()) == QDate()); QVERIFY(sc3.readEntry("badList", QDateTime()) == QDateTime()); // 5 element list list[2] = 3; list << 5; sc3.writeEntry("badList", list); QVERIFY(sc3.readEntry("badList", QPoint()) == QPoint()); QVERIFY(sc3.readEntry("badList", QRect()) == QRect()); QVERIFY(sc3.readEntry("badList", QSize()) == QSize()); QVERIFY(sc3.readEntry("badList", QDate()) == QDate()); QVERIFY(sc3.readEntry("badList", QDateTime()) == QDateTime()); // 6 element list list << 6; sc3.writeEntry("badList", list); QVERIFY(sc3.readEntry("badList", QPoint()) == QPoint()); QVERIFY(sc3.readEntry("badList", QRect()) == QRect()); QVERIFY(sc3.readEntry("badList", QSize()) == QSize()); } void KConfigTest::testChangeGroup() { KConfig sc(TEST_SUBDIR "kconfigtest"); KConfigGroup sc3(&sc, "Hello"); QCOMPARE(sc3.name(), QString("Hello")); KConfigGroup newGroup(sc3); #if KCONFIGCORE_ENABLE_DEPRECATED_SINCE(5, 0) newGroup.changeGroup("FooBar"); // deprecated! QCOMPARE(newGroup.name(), QString("FooBar")); QCOMPARE(sc3.name(), QString("Hello")); // unchanged // Write into the "changed group" and check that it works newGroup.writeEntry("InFooBar", "FB"); QCOMPARE(KConfigGroup(&sc, "FooBar").entryMap().value("InFooBar"), QString("FB")); QCOMPARE(KConfigGroup(&sc, "Hello").entryMap().value("InFooBar"), QString()); #endif KConfigGroup rootGroup(sc.group("")); QCOMPARE(rootGroup.name(), QString("")); KConfigGroup sc32(rootGroup.group("Hello")); QCOMPARE(sc32.name(), QString("Hello")); KConfigGroup newGroup2(sc32); #if KCONFIGCORE_ENABLE_DEPRECATED_SINCE(5, 0) newGroup2.changeGroup("FooBar"); // deprecated! QCOMPARE(newGroup2.name(), QString("FooBar")); QCOMPARE(sc32.name(), QString("Hello")); // unchanged #endif } // Simple test for deleteEntry void KConfigTest::testDeleteEntry() { const char *configFile = TEST_SUBDIR "kconfigdeletetest"; { KConfig conf(configFile); conf.group("Hello").writeEntry("DelKey", "ToBeDeleted"); } const QList lines = readLines(configFile); Q_ASSERT(lines.contains("[Hello]\n")); Q_ASSERT(lines.contains("DelKey=ToBeDeleted\n")); KConfig sc(configFile); KConfigGroup group(&sc, "Hello"); group.deleteEntry("DelKey"); QCOMPARE(group.readEntry("DelKey", QString("Fietsbel")), QString("Fietsbel")); QVERIFY(group.sync()); Q_ASSERT(!readLines(configFile).contains("DelKey=ToBeDeleted\n")); QCOMPARE(group.readEntry("DelKey", QString("still deleted")), QString("still deleted")); } void KConfigTest::testDelete() { KConfig sc(TEST_SUBDIR "kconfigtest"); KConfigGroup ct(&sc, "Complex Types"); // First delete a nested group KConfigGroup delgr(&ct, "Nested Group 3"); QVERIFY(delgr.exists()); QVERIFY(ct.hasGroup("Nested Group 3")); delgr.deleteGroup(); QVERIFY(!delgr.exists()); QVERIFY(!ct.hasGroup("Nested Group 3")); QVERIFY(ct.groupList().contains("Nested Group 3")); KConfigGroup ng(&ct, "Nested Group 2"); QVERIFY(sc.hasGroup("Complex Types")); QVERIFY(!sc.hasGroup("Does not exist")); sc.deleteGroup("Complex Types"); QCOMPARE(sc.group("Complex Types").keyList().count(), 0); QVERIFY(!sc.hasGroup("Complex Types")); // #192266 QVERIFY(!sc.group("Complex Types").exists()); QVERIFY(!ct.hasGroup("Nested Group 1")); QCOMPARE(ct.group("Nested Group 1").keyList().count(), 0); QCOMPARE(ct.group("Nested Group 2").keyList().count(), 0); QCOMPARE(ng.group("Nested Group 2.1").keyList().count(), 0); KConfigGroup cg(&sc, "AAA"); cg.deleteGroup(); QVERIFY(sc.entryMap("Complex Types").isEmpty()); QVERIFY(sc.entryMap("AAA").isEmpty()); QVERIFY(!sc.entryMap("Hello").isEmpty()); //not deleted group QVERIFY(sc.entryMap("FooBar").isEmpty()); //inexistant group QVERIFY(cg.sync()); // Check what happens on disk const QList lines = readLines(); //qDebug() << lines; QVERIFY(!lines.contains("[Complex Types]\n")); QVERIFY(!lines.contains("[Complex Types][Nested Group 1]\n")); QVERIFY(!lines.contains("[Complex Types][Nested Group 2]\n")); QVERIFY(!lines.contains("[Complex Types][Nested Group 2.1]\n")); QVERIFY(!lines.contains("[AAA]\n")); QVERIFY(lines.contains("[Hello]\n")); // a group that was not deleted // test for entries that are marked as deleted when there is no default KConfig cf(TEST_SUBDIR "kconfigtest", KConfig::SimpleConfig); // make sure there are no defaults cg = cf.group("Portable Devices"); cg.writeEntry("devices|manual|(null)", "whatever"); cg.writeEntry("devices|manual|/mnt/ipod", "/mnt/ipod"); QVERIFY(cf.sync()); int count = 0; const QList listLines = readLines(); for (const QByteArray &item : listLines) if (item.startsWith("devices|")) { // krazy:exclude=strings count++; } QCOMPARE(count, 2); cg.deleteEntry("devices|manual|/mnt/ipod"); QVERIFY(cf.sync()); const QList listLines2 = readLines(); for (const QByteArray &item : listLines2) { QVERIFY(!item.contains("ipod")); } } void KConfigTest::testDefaultGroup() { KConfig sc(TEST_SUBDIR "kconfigtest"); KConfigGroup defaultGroup(&sc, ""); QCOMPARE(defaultGroup.name(), QString("")); QVERIFY(!defaultGroup.exists()); defaultGroup.writeEntry("TestKey", "defaultGroup"); QVERIFY(defaultGroup.exists()); QCOMPARE(defaultGroup.readEntry("TestKey", QString()), QString("defaultGroup")); QVERIFY(sc.sync()); { // Test reading it KConfig sc2(TEST_SUBDIR "kconfigtest"); KConfigGroup defaultGroup2(&sc2, ""); QCOMPARE(defaultGroup2.name(), QString("")); QVERIFY(defaultGroup2.exists()); QCOMPARE(defaultGroup2.readEntry("TestKey", QString()), QString("defaultGroup")); } { // Test reading it KConfig sc2(TEST_SUBDIR "kconfigtest"); KConfigGroup emptyGroup(&sc2, ""); QCOMPARE(emptyGroup.name(), QString("")); QVERIFY(emptyGroup.exists()); QCOMPARE(emptyGroup.readEntry("TestKey", QString()), QString("defaultGroup")); } QList lines = readLines(); QVERIFY(!lines.contains("[]\n")); QCOMPARE(lines.first(), QByteArray("TestKey=defaultGroup\n")); // Now that the group exists make sure it isn't returned from groupList() const QStringList groupList = sc.groupList(); for (const QString &group : groupList) { QVERIFY(!group.isEmpty() && group != ""); } defaultGroup.deleteGroup(); QVERIFY(sc.sync()); // Test if deleteGroup worked lines = readLines(); QVERIFY(lines.first() != QByteArray("TestKey=defaultGroup\n")); } void KConfigTest::testEmptyGroup() { KConfig sc(TEST_SUBDIR "kconfigtest"); KConfigGroup emptyGroup(&sc, ""); QCOMPARE(emptyGroup.name(), QString("")); // confusing, heh? QVERIFY(!emptyGroup.exists()); emptyGroup.writeEntry("TestKey", "emptyGroup"); QVERIFY(emptyGroup.exists()); QCOMPARE(emptyGroup.readEntry("TestKey", QString()), QString("emptyGroup")); QVERIFY(sc.sync()); { // Test reading it KConfig sc2(TEST_SUBDIR "kconfigtest"); KConfigGroup defaultGroup(&sc2, ""); QCOMPARE(defaultGroup.name(), QString("")); QVERIFY(defaultGroup.exists()); QCOMPARE(defaultGroup.readEntry("TestKey", QString()), QString("emptyGroup")); } { // Test reading it KConfig sc2(TEST_SUBDIR "kconfigtest"); KConfigGroup emptyGroup2(&sc2, ""); QCOMPARE(emptyGroup2.name(), QString("")); QVERIFY(emptyGroup2.exists()); QCOMPARE(emptyGroup2.readEntry("TestKey", QString()), QString("emptyGroup")); } QList lines = readLines(); QVERIFY(!lines.contains("[]\n")); // there's no support for the [] group, in fact. QCOMPARE(lines.first(), QByteArray("TestKey=emptyGroup\n")); // Now that the group exists make sure it isn't returned from groupList() const QStringList groupList = sc.groupList(); for (const QString &group : groupList) { QVERIFY(!group.isEmpty() && group != ""); } emptyGroup.deleteGroup(); QVERIFY(sc.sync()); // Test if deleteGroup worked lines = readLines(); QVERIFY(lines.first() != QByteArray("TestKey=defaultGroup\n")); } #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) && !defined(Q_OS_BLACKBERRY) && !defined(Q_OS_ANDROID) #define Q_XDG_PLATFORM #endif void KConfigTest::testCascadingWithLocale() { // This test relies on XDG_CONFIG_DIRS, which only has effect on Unix. // Cascading (more than two levels) isn't available at all on Windows. #ifdef Q_XDG_PLATFORM QTemporaryDir middleDir; QTemporaryDir globalDir; qputenv("XDG_CONFIG_DIRS", qPrintable(middleDir.path() + QString(":") + globalDir.path())); const QString globalConfigDir = globalDir.path() + "/" TEST_SUBDIR; QVERIFY(QDir().mkpath(globalConfigDir)); QFile global(globalConfigDir + "foo.desktop"); QVERIFY(global.open(QIODevice::WriteOnly | QIODevice::Text)); QTextStream globalOut(&global); globalOut << "[Group]\n" << "FromGlobal=true\n" << "FromGlobal[fr]=vrai\n" << "Name=Testing\n" << "Name[fr]=FR\n" << "Other=Global\n" << "Other[fr]=Global_FR\n"; global.close(); const QString middleConfigDir = middleDir.path() + "/" TEST_SUBDIR; QVERIFY(QDir().mkpath(middleConfigDir)); QFile local(middleConfigDir + "foo.desktop"); QVERIFY(local.open(QIODevice::WriteOnly | QIODevice::Text)); QTextStream out(&local); out << "[Group]\n" << "FromLocal=true\n" << "FromLocal[fr]=vrai\n" << "Name=Local Testing\n" << "Name[fr]=FR\n" << "Other=English Only\n"; local.close(); KConfig config(TEST_SUBDIR "foo.desktop"); KConfigGroup group = config.group("Group"); QCOMPARE(group.readEntry("FromGlobal"), QString("true")); QCOMPARE(group.readEntry("FromLocal"), QString("true")); QCOMPARE(group.readEntry("Name"), QString("Local Testing")); config.setLocale(QStringLiteral("fr")); QCOMPARE(group.readEntry("FromGlobal"), QString("vrai")); QCOMPARE(group.readEntry("FromLocal"), QString("vrai")); QCOMPARE(group.readEntry("Name"), QString("FR")); QCOMPARE(group.readEntry("Other"), QString("English Only")); // Global_FR is locally overriden #endif } void KConfigTest::testMerge() { DefaultLocale defaultLocale; QLocale::setDefault(QLocale::c()); KConfig config(TEST_SUBDIR "mergetest", KConfig::SimpleConfig); KConfigGroup cg = config.group("some group"); cg.writeEntry("entry", " random entry"); cg.writeEntry("another entry", "blah blah blah"); { // simulate writing by another process QFile file(testConfigDir() + "/mergetest"); file.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream out(&file); out.setCodec("UTF-8"); out << "[Merged Group]\n" << "entry1=Testing\n" << "entry2=More Testing\n" << "[some group]\n" << "entry[fr]=French\n" << "entry[es]=Spanish\n" << "entry[de]=German\n"; } QVERIFY(config.sync()); { QList lines; // this is what the file should look like lines << "[Merged Group]\n" << "entry1=Testing\n" << "entry2=More Testing\n" << "\n" << "[some group]\n" << "another entry=blah blah blah\n" << "entry=\\srandom entry\n" << "entry[de]=German\n" << "entry[es]=Spanish\n" << "entry[fr]=French\n"; QFile file(testConfigDir() + "/mergetest"); file.open(QIODevice::ReadOnly | QIODevice::Text); for (const QByteArray &line : qAsConst(lines)) { QCOMPARE(line, file.readLine()); } } } void KConfigTest::testImmutable() { { QFile file(testConfigDir() + "/immutabletest"); file.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream out(&file); out.setCodec("UTF-8"); out << "[$i]\n" << "entry1=Testing\n" << "[group][$i]\n" << "[group][subgroup][$i]\n"; } KConfig config(TEST_SUBDIR "immutabletest", KConfig::SimpleConfig); QVERIFY(config.isGroupImmutable(QByteArray())); KConfigGroup cg = config.group(QByteArray()); QVERIFY(cg.isEntryImmutable("entry1")); KConfigGroup cg1 = config.group("group"); QVERIFY(cg1.isImmutable()); KConfigGroup cg1a = cg.group("group"); QVERIFY(cg1a.isImmutable()); KConfigGroup cg2 = cg1.group("subgroup"); QVERIFY(cg2.isImmutable()); } void KConfigTest::testOptionOrder() { { QFile file(testConfigDir() + "/doubleattrtest"); file.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream out(&file); out.setCodec("UTF-8"); out << "[group3]\n" << "entry2=unlocalized\n" << "entry2[$i][de_DE]=t2\n"; } KConfig config(TEST_SUBDIR "doubleattrtest", KConfig::SimpleConfig); config.setLocale(QStringLiteral("de_DE")); KConfigGroup cg3 = config.group("group3"); QVERIFY(!cg3.isImmutable()); QCOMPARE(cg3.readEntry("entry2", ""), QString("t2")); QVERIFY(cg3.isEntryImmutable("entry2")); config.setLocale(QStringLiteral("C")); QCOMPARE(cg3.readEntry("entry2", ""), QString("unlocalized")); QVERIFY(!cg3.isEntryImmutable("entry2")); cg3.writeEntry("entry2", "modified"); QVERIFY(config.sync()); { QList lines; // this is what the file should look like lines << "[group3]\n" << "entry2=modified\n" << "entry2[de_DE][$i]=t2\n"; QFile file(testConfigDir() + "/doubleattrtest"); file.open(QIODevice::ReadOnly | QIODevice::Text); for (const QByteArray &line : qAsConst(lines)) { QCOMPARE(line, file.readLine()); } } } void KConfigTest::testGroupEscape() { KConfig config(TEST_SUBDIR "groupescapetest", KConfig::SimpleConfig); QVERIFY(config.group(DOLLARGROUP).exists()); } void KConfigTest::testSubGroup() { KConfig sc(TEST_SUBDIR "kconfigtest"); KConfigGroup cg(&sc, "ParentGroup"); QCOMPARE(cg.readEntry("parentgrpstring", ""), QString("somevalue")); KConfigGroup subcg1(&cg, "SubGroup1"); QCOMPARE(subcg1.name(), QString("SubGroup1")); QCOMPARE(subcg1.readEntry("somestring", ""), QString("somevalue")); KConfigGroup subcg2(&cg, "SubGroup2"); QCOMPARE(subcg2.name(), QString("SubGroup2")); QCOMPARE(subcg2.readEntry("substring", ""), QString("somevalue")); KConfigGroup subcg3(&cg, "SubGroup/3"); QCOMPARE(subcg3.readEntry("sub3string", ""), QString("somevalue")); QCOMPARE(subcg3.name(), QString("SubGroup/3")); KConfigGroup rcg(&sc, ""); KConfigGroup srcg(&rcg, "ParentGroup"); QCOMPARE(srcg.readEntry("parentgrpstring", ""), QString("somevalue")); QStringList groupList = cg.groupList(); groupList.sort(); // comes from QSet, so order is undefined QCOMPARE(groupList, (QStringList() << "SubGroup/3" << "SubGroup1" << "SubGroup2")); const QStringList expectedSubgroup3Keys = (QStringList() << QStringLiteral("sub3string")); QCOMPARE(subcg3.keyList(), expectedSubgroup3Keys); const QStringList expectedParentGroupKeys(QStringList() << QStringLiteral("parentgrpstring")); QCOMPARE(cg.keyList(), expectedParentGroupKeys); QCOMPARE(QStringList(cg.entryMap().keys()), expectedParentGroupKeys); QCOMPARE(QStringList(subcg3.entryMap().keys()), expectedSubgroup3Keys); // Create A group containing only other groups. We want to make sure it // shows up in groupList of sc KConfigGroup neg(&sc, "NoEntryGroup"); KConfigGroup negsub1(&neg, "NEG Child1"); negsub1.writeEntry("entry", "somevalue"); KConfigGroup negsub2(&neg, "NEG Child2"); KConfigGroup negsub3(&neg, "NEG Child3"); KConfigGroup negsub31(&negsub3, "NEG Child3-1"); KConfigGroup negsub4(&neg, "NEG Child4"); KConfigGroup negsub41(&negsub4, "NEG Child4-1"); negsub41.writeEntry("entry", "somevalue"); // A group exists if it has content QVERIFY(negsub1.exists()); // But it doesn't exist if it has no content // Ossi and David say: this is how it's supposed to work. // However you could add a dummy entry for now, or we could add a "Persist" feature to kconfig groups // which would make it written out, much like "immutable" already makes them persistent. QVERIFY(!negsub2.exists()); // A subgroup does not qualify as content if it is also empty QVERIFY(!negsub3.exists()); // A subgroup with content is ok QVERIFY(negsub4.exists()); // Only subgroups with content show up in groupList() //QEXPECT_FAIL("", "Empty subgroups do not show up in groupList()", Continue); //QCOMPARE(neg.groupList(), QStringList() << "NEG Child1" << "NEG Child2" << "NEG Child3" << "NEG Child4"); // This is what happens QStringList groups = neg.groupList(); groups.sort(); // Qt5 made the ordering unreliable, due to QHash QCOMPARE(groups, QStringList() << "NEG Child1" << "NEG Child4"); // make sure groupList() isn't returning something it shouldn't const QStringList listGroup = sc.groupList(); for (const QString &group : listGroup) { QVERIFY(!group.isEmpty() && group != ""); QVERIFY(!group.contains(QChar(0x1d))); QVERIFY(!group.contains("subgroup")); QVERIFY(!group.contains("SubGroup")); } QVERIFY(sc.sync()); // Check that the empty groups are not written out. const QList lines = readLines(); QVERIFY(lines.contains("[NoEntryGroup][NEG Child1]\n")); QVERIFY(!lines.contains("[NoEntryGroup][NEG Child2]\n")); QVERIFY(!lines.contains("[NoEntryGroup][NEG Child3]\n")); QVERIFY(!lines.contains("[NoEntryGroup][NEG Child4]\n")); // implicit group, not written out QVERIFY(lines.contains("[NoEntryGroup][NEG Child4][NEG Child4-1]\n")); } void KConfigTest::testAddConfigSources() { KConfig cf(TEST_SUBDIR "specificrc"); cf.addConfigSources(QStringList() << testConfigDir() + "/baserc"); cf.reparseConfiguration(); KConfigGroup specificgrp(&cf, "Specific Only Group"); QCOMPARE(specificgrp.readEntry("ExistingEntry", ""), QString("DevValue")); KConfigGroup sharedgrp(&cf, "Shared Group"); QCOMPARE(sharedgrp.readEntry("SomeSpecificOnlyEntry", ""), QString("DevValue")); QCOMPARE(sharedgrp.readEntry("SomeBaseOnlyEntry", ""), QString("BaseValue")); QCOMPARE(sharedgrp.readEntry("SomeSharedEntry", ""), QString("DevValue")); KConfigGroup basegrp(&cf, "Base Only Group"); QCOMPARE(basegrp.readEntry("ExistingEntry", ""), QString("BaseValue")); basegrp.writeEntry("New Entry Base Only", "SomeValue"); KConfigGroup newgrp(&cf, "New Group"); newgrp.writeEntry("New Entry", "SomeValue"); QVERIFY(cf.sync()); KConfig plaincfg(TEST_SUBDIR "specificrc"); KConfigGroup newgrp2(&plaincfg, "New Group"); QCOMPARE(newgrp2.readEntry("New Entry", ""), QString("SomeValue")); KConfigGroup basegrp2(&plaincfg, "Base Only Group"); QCOMPARE(basegrp2.readEntry("New Entry Base Only", ""), QString("SomeValue")); } void KConfigTest::testGroupCopyTo() { KConfig cf1(TEST_SUBDIR "kconfigtest"); KConfigGroup original = cf1.group("Enum Types"); KConfigGroup copy = cf1.group("Enum Types Copy"); original.copyTo(©); // copy from one group to another QCOMPARE(copy.entryMap(), original.entryMap()); KConfig cf2(TEST_SUBDIR "copy_of_kconfigtest", KConfig::SimpleConfig); QVERIFY(!cf2.hasGroup(original.name())); QVERIFY(!cf2.hasGroup(copy.name())); KConfigGroup newGroup = cf2.group(original.name()); original.copyTo(&newGroup); // copy from one file to another QVERIFY(cf2.hasGroup(original.name())); QVERIFY(!cf2.hasGroup(copy.name())); // make sure we didn't copy more than we wanted QCOMPARE(newGroup.entryMap(), original.entryMap()); } void KConfigTest::testConfigCopyToSync() { KConfig cf1(TEST_SUBDIR "kconfigtest"); // Prepare source file KConfigGroup group(&cf1, "CopyToTest"); group.writeEntry("Type", "Test"); QVERIFY(cf1.sync()); // Copy to "destination" const QString destination = testConfigDir() + "/kconfigcopytotest"; QFile::remove(destination); KConfig cf2(TEST_SUBDIR "kconfigcopytotest"); KConfigGroup group2(&cf2, "CopyToTest"); group.copyTo(&group2); QString testVal = group2.readEntry("Type"); QCOMPARE(testVal, QString("Test")); // should write to disk the copied data from group QVERIFY(cf2.sync()); QVERIFY(QFile::exists(destination)); } void KConfigTest::testConfigCopyTo() { KConfig cf1(TEST_SUBDIR "kconfigtest"); { // Prepare source file KConfigGroup group(&cf1, "CopyToTest"); group.writeEntry("Type", "Test"); QVERIFY(cf1.sync()); } { // Copy to "destination" const QString destination = testConfigDir() + "/kconfigcopytotest"; QFile::remove(destination); KConfig cf2; cf1.copyTo(destination, &cf2); KConfigGroup group2(&cf2, "CopyToTest"); QString testVal = group2.readEntry("Type"); QCOMPARE(testVal, QString("Test")); QVERIFY(cf2.sync()); QVERIFY(QFile::exists(destination)); } // Check copied config file on disk KConfig cf3(TEST_SUBDIR "kconfigcopytotest"); KConfigGroup group3(&cf3, "CopyToTest"); QString testVal = group3.readEntry("Type"); QCOMPARE(testVal, QString("Test")); } void KConfigTest::testReparent() { KConfig cf(TEST_SUBDIR "kconfigtest"); const QString name(QStringLiteral("Enum Types")); KConfigGroup group = cf.group(name); const QMap originalMap = group.entryMap(); KConfigGroup parent = cf.group("Parent Group"); QVERIFY(!parent.hasGroup(name)); QVERIFY(group.entryMap() == originalMap); group.reparent(&parent); // see if it can be made a sub-group of another group QVERIFY(parent.hasGroup(name)); QCOMPARE(group.entryMap(), originalMap); group.reparent(&cf); // see if it can make it a top-level group again // QVERIFY(!parent.hasGroup(name)); QCOMPARE(group.entryMap(), originalMap); } static void ageTimeStamp(const QString &path, int nsec) { #ifdef Q_OS_UNIX QDateTime mtime = QFileInfo(path).lastModified().addSecs(-nsec); struct utimbuf utbuf; utbuf.actime = mtime.toSecsSinceEpoch(); utbuf.modtime = utbuf.actime; utime(QFile::encodeName(path), &utbuf); #else QTest::qSleep(nsec * 1000); #endif } void KConfigTest::testWriteOnSync() { QDateTime oldStamp, newStamp; KConfig sc(TEST_SUBDIR "kconfigtest", KConfig::IncludeGlobals); // Age the timestamp of global config file a few sec, and collect it. QString globFile = kdeGlobalsPath(); ageTimeStamp(globFile, 2); // age 2 sec oldStamp = QFileInfo(globFile).lastModified(); // Add a local entry and sync the config. // Should not rewrite the global config file. KConfigGroup cgLocal(&sc, "Locals"); cgLocal.writeEntry("someLocalString", "whatever"); QVERIFY(sc.sync()); // Verify that the timestamp of global config file didn't change. newStamp = QFileInfo(globFile).lastModified(); QCOMPARE(newStamp, oldStamp); // Age the timestamp of local config file a few sec, and collect it. QString locFile = testConfigDir() + "/kconfigtest"; ageTimeStamp(locFile, 2); // age 2 sec oldStamp = QFileInfo(locFile).lastModified(); // Add a global entry and sync the config. // Should not rewrite the local config file. KConfigGroup cgGlobal(&sc, "Globals"); cgGlobal.writeEntry("someGlobalString", "whatever", KConfig::Persistent | KConfig::Global); QVERIFY(sc.sync()); // Verify that the timestamp of local config file didn't change. newStamp = QFileInfo(locFile).lastModified(); QCOMPARE(newStamp, oldStamp); } void KConfigTest::testFailOnReadOnlyFileSync() { KConfig sc(TEST_SUBDIR "kconfigfailonreadonlytest"); KConfigGroup cgLocal(&sc, "Locals"); cgLocal.writeEntry("someLocalString", "whatever"); QVERIFY(cgLocal.sync()); QFile f(testConfigDir() + "kconfigfailonreadonlytest"); QVERIFY(f.exists()); QVERIFY(f.setPermissions(QFileDevice::ReadOwner)); #ifndef Q_OS_WIN if (::getuid() == 0) QSKIP("Root can write to read-only files"); #endif cgLocal.writeEntry("someLocalString", "whatever2"); QVERIFY(!cgLocal.sync()); QVERIFY(f.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner)); QVERIFY(f.remove()); } void KConfigTest::testDirtyOnEqual() { QDateTime oldStamp, newStamp; KConfig sc(TEST_SUBDIR "kconfigtest"); // Initialize value KConfigGroup cgLocal(&sc, "random"); cgLocal.writeEntry("theKey", "whatever"); QVERIFY(sc.sync()); // Age the timestamp of local config file a few sec, and collect it. QString locFile = testConfigDir() + "/kconfigtest"; ageTimeStamp(locFile, 2); // age 2 sec oldStamp = QFileInfo(locFile).lastModified(); // Write exactly the same again cgLocal.writeEntry("theKey", "whatever"); // This should be a no-op QVERIFY(sc.sync()); // Verify that the timestamp of local config file didn't change. newStamp = QFileInfo(locFile).lastModified(); QCOMPARE(newStamp, oldStamp); } void KConfigTest::testDirtyOnEqualOverdo() { QByteArray val1("\0""one", 4); QByteArray val2("\0""two", 4); QByteArray defvalr; KConfig sc(TEST_SUBDIR "kconfigtest"); KConfigGroup cgLocal(&sc, "random"); cgLocal.writeEntry("someKey", val1); QCOMPARE(cgLocal.readEntry("someKey", defvalr), val1); cgLocal.writeEntry("someKey", val2); QCOMPARE(cgLocal.readEntry("someKey", defvalr), val2); } void KConfigTest::testCreateDir() { // Test auto-creating the parent directory when needed (KConfigIniBackend::createEnclosing) QString kdehome = QDir::home().canonicalPath() + "/.kde-unit-test"; QString subdir = kdehome + "/newsubdir"; QString file = subdir + "/foo.desktop"; QFile::remove(file); QDir().rmdir(subdir); QVERIFY(!QDir().exists(subdir)); KDesktopFile desktopFile(file); desktopFile.desktopGroup().writeEntry("key", "value"); QVERIFY(desktopFile.sync()); QVERIFY(QFile::exists(file)); // Cleanup QFile::remove(file); QDir().rmdir(subdir); } void KConfigTest::testSyncOnExit() { // Often, the KGlobalPrivate global static's destructor ends up calling ~KConfig -> // KConfig::sync ... and if that code triggers KGlobal code again then things could crash. // So here's a test for modifying KSharedConfig::openConfig() and not syncing, the process exit will sync. KConfigGroup grp(KSharedConfig::openConfig(TEST_SUBDIR "syncOnExitRc"), "syncOnExit"); grp.writeEntry("key", "value"); } void KConfigTest::testSharedConfig() { // Can I use a KConfigGroup even after the KSharedConfigPtr goes out of scope? KConfigGroup myConfigGroup; { KSharedConfigPtr config = KSharedConfig::openConfig(TEST_SUBDIR "kconfigtest"); myConfigGroup = KConfigGroup(config, "Hello"); } QCOMPARE(myConfigGroup.readEntry("stringEntry1"), QString(STRINGENTRY1)); // Get the main config KSharedConfigPtr mainConfig = KSharedConfig::openConfig(); KConfigGroup mainGroup(mainConfig, "Main"); QCOMPARE(mainGroup.readEntry("Key", QString()), QString("Value")); } void KConfigTest::testLocaleConfig() { // Initialize the testdata QDir dir; QString subdir = testConfigDir(); dir.mkpath(subdir); QString file = subdir + "/localized.test"; QFile::remove(file); QFile f(file); QVERIFY(f.open(QIODevice::WriteOnly)); QTextStream ts(&f); ts << "[Test_Wrong]\n"; ts << "foo[ca]=5\n"; ts << "foostring[ca]=nice\n"; ts << "foobool[ca]=true\n"; ts << "[Test_Right]\n"; ts << "foo=5\n"; ts << "foo[ca]=5\n"; ts << "foostring=primary\n"; ts << "foostring[ca]=nice\n"; ts << "foobool=primary\n"; ts << "foobool[ca]=true\n"; f.close(); // Load the testdata QVERIFY(QFile::exists(file)); KConfig config(file); config.setLocale(QStringLiteral("ca")); // This group has only localized values. That is not supported. The values // should be dropped on loading. KConfigGroup cg(&config, "Test_Wrong"); QEXPECT_FAIL("", "The localized values are not dropped", Continue); QVERIFY(!cg.hasKey("foo")); QEXPECT_FAIL("", "The localized values are not dropped", Continue); QVERIFY(!cg.hasKey("foostring")); QEXPECT_FAIL("", "The localized values are not dropped", Continue); QVERIFY(!cg.hasKey("foobool")); // Now check the correct config group KConfigGroup cg2(&config, "Test_Right"); QCOMPARE(cg2.readEntry("foo"), QString("5")); QCOMPARE(cg2.readEntry("foo", 3), 5); QCOMPARE(cg2.readEntry("foostring"), QString("nice")); QCOMPARE(cg2.readEntry("foostring", "ugly"), QString("nice")); QCOMPARE(cg2.readEntry("foobool"), QString("true")); QCOMPARE(cg2.readEntry("foobool", false), true); // Clean up after the testcase QFile::remove(file); } void KConfigTest::testDeleteWhenLocalized() { // Initialize the testdata QDir dir; QString subdir = QDir::home().canonicalPath() + "/.kde-unit-test/"; dir.mkpath(subdir); QString file = subdir + "/localized_delete.test"; QFile::remove(file); QFile f(file); QVERIFY(f.open(QIODevice::WriteOnly)); QTextStream ts(&f); ts << "[Test4711]\n"; ts << "foo=3\n"; ts << "foo[ca]=5\n"; ts << "foo[de]=7\n"; ts << "foostring=ugly\n"; ts << "foostring[ca]=nice\n"; ts << "foostring[de]=schoen\n"; ts << "foobool=false\n"; ts << "foobool[ca]=true\n"; ts << "foobool[de]=true\n"; f.close(); // Load the testdata. We start in locale "ca". QVERIFY(QFile::exists(file)); KConfig config(file); config.setLocale(QStringLiteral("ca")); KConfigGroup cg(&config, "Test4711"); // Delete a value. Once with localized, once with Normal cg.deleteEntry("foostring", KConfigBase::Persistent | KConfigBase::Localized); cg.deleteEntry("foobool"); QVERIFY(config.sync()); // The value is now gone. The others are still there. Everything correct // here. QVERIFY(!cg.hasKey("foostring")); QVERIFY(!cg.hasKey("foobool")); QVERIFY(cg.hasKey("foo")); // The current state is: (Just return before this comment.) // [...] // foobool[ca]=true // foobool[de]=wahr // foostring=ugly // foostring[de]=schoen // Now switch the locale to "de" and repeat the checks. Results should be // the same. But they currently are not. The localized value are // independent of each other. All values are still there in "de". config.setLocale(QStringLiteral("de")); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foostring")); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foobool")); QVERIFY(cg.hasKey("foo")); // Check where the wrong values come from. // We get the "de" value. QCOMPARE(cg.readEntry("foostring", "nothing"), QString("schoen")); // We get the "de" value. QCOMPARE(cg.readEntry("foobool", false), true); // Now switch the locale back "ca" and repeat the checks. Results are // again different. config.setLocale(QStringLiteral("ca")); // This line worked above. But now it fails. QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foostring")); // This line worked above too. QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foobool")); QVERIFY(cg.hasKey("foo")); // Check where the wrong values come from. // We get the primary value because the "ca" value was deleted. QCOMPARE(cg.readEntry("foostring", "nothing"), QString("ugly")); // We get the "ca" value. QCOMPARE(cg.readEntry("foobool", false), true); // Now test the deletion of a group. cg.deleteGroup(); QVERIFY(config.sync()); // Current state: [ca] and [de] entries left... oops. //qDebug() << readLinesFrom(file); // Bug: The group still exists [because of the localized entries]... QVERIFY(cg.exists()); QVERIFY(!cg.hasKey("foo")); QVERIFY(!cg.hasKey("foostring")); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foobool")); // Now switch the locale to "de" and repeat the checks. All values // still here because only the primary values are deleted. config.setLocale(QStringLiteral("de")); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foo")); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foostring")); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foobool")); // Check where the wrong values come from. // We get the "de" value. QCOMPARE(cg.readEntry("foostring", "nothing"), QString("schoen")); // We get the "de" value. QCOMPARE(cg.readEntry("foobool", false), true); // We get the "de" value. QCOMPARE(cg.readEntry("foo", 0), 7); // Now switch the locale to "ca" and repeat the checks // "foostring" is now really gone because both the primary value and the // "ca" value are deleted. config.setLocale(QStringLiteral("ca")); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foo")); QVERIFY(!cg.hasKey("foostring")); QEXPECT_FAIL("", "Currently localized values are not deleted correctly", Continue); QVERIFY(!cg.hasKey("foobool")); // Check where the wrong values come from. // We get the "ca" value. QCOMPARE(cg.readEntry("foobool", false), true); // We get the "ca" value. QCOMPARE(cg.readEntry("foo", 0), 5); // Cleanup QFile::remove(file); } void KConfigTest::testKdeGlobals() { { KConfig glob(QStringLiteral("kdeglobals")); KConfigGroup general(&glob, "General"); general.writeEntry("testKG", "1"); QVERIFY(glob.sync()); } KConfig globRead(QStringLiteral("kdeglobals")); const KConfigGroup general(&globRead, "General"); QCOMPARE(general.readEntry("testKG"), QString("1")); // Check we wrote into kdeglobals const QList lines = readLines("kdeglobals"); QVERIFY(lines.contains("[General]\n")); QVERIFY(lines.contains("testKG=1\n")); // Writing using NoGlobals { KConfig glob(QStringLiteral("kdeglobals"), KConfig::NoGlobals); KConfigGroup general(&glob, "General"); general.writeEntry("testKG", "2"); QVERIFY(glob.sync()); } globRead.reparseConfiguration(); QCOMPARE(general.readEntry("testKG"), QString("2")); // Reading using NoGlobals { KConfig globReadNoGlob(QStringLiteral("kdeglobals"), KConfig::NoGlobals); const KConfigGroup generalNoGlob(&globReadNoGlob, "General"); QCOMPARE(generalNoGlob.readEntry("testKG"), QString("2")); } // TODO now use kconfigtest and writeEntry(,Global) -> should go into kdeglobals } void KConfigTest::testAnonymousConfig() { KConfig anonConfig(QString(), KConfig::SimpleConfig); KConfigGroup general(&anonConfig, "General"); QCOMPARE(general.readEntry("testKG"), QString()); // no kdeglobals merging general.writeEntry("Foo", "Bar"); QCOMPARE(general.readEntry("Foo"), QString("Bar")); } void KConfigTest::testQByteArrayUtf8() { QTemporaryFile file; QVERIFY(file.open()); KConfig config(file.fileName(), KConfig::SimpleConfig); KConfigGroup general(&config, "General"); QByteArray bytes(256, '\0'); for (int i = 0; i < 256; i++) { bytes[i] = i; } general.writeEntry("Utf8", bytes); config.sync(); file.flush(); file.close(); QFile readFile(file.fileName()); QVERIFY(readFile.open(QFile::ReadOnly)); #define VALUE "Utf8=\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\t\\n\\x0b\\x0c\\r\\x0e\\x0f\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\\x7f\\x80\\x81\\x82\\x83\\x84\\x85\\x86\\x87\\x88\\x89\\x8a\\x8b\\x8c\\x8d\\x8e\\x8f\\x90\\x91\\x92\\x93\\x94\\x95\\x96\\x97\\x98\\x99\\x9a\\x9b\\x9c\\x9d\\x9e\\x9f\\xa0\\xa1\\xa2\\xa3\\xa4\\xa5\\xa6\\xa7\\xa8\\xa9\\xaa\\xab\\xac\\xad\\xae\\xaf\\xb0\\xb1\\xb2\\xb3\\xb4\\xb5\\xb6\\xb7\\xb8\\xb9\\xba\\xbb\\xbc\\xbd\\xbe\\xbf\\xc0\\xc1\\xc2\\xc3\\xc4\\xc5\\xc6\\xc7\\xc8\\xc9\\xca\\xcb\\xcc\\xcd\\xce\\xcf\\xd0\\xd1\\xd2\\xd3\\xd4\\xd5\\xd6\\xd7\\xd8\\xd9\\xda\\xdb\\xdc\\xdd\\xde\\xdf\\xe0\\xe1\\xe2\\xe3\\xe4\\xe5\\xe6\\xe7\\xe8\\xe9\\xea\\xeb\\xec\\xed\\xee\\xef\\xf0\\xf1\\xf2\\xf3\\xf4\\xf5\\xf6\\xf7\\xf8\\xf9\\xfa\\xfb\\xfc\\xfd\\xfe\\xff" const QByteArray fileBytes = readFile.readAll(); #ifndef Q_OS_WIN QCOMPARE(fileBytes, QByteArrayLiteral("[General]\n" VALUE "\n")); #else QCOMPARE(fileBytes, QByteArrayLiteral("[General]\r\n" VALUE "\r\n")); #endif #undef VALUE // check that reading works KConfig config2(file.fileName(), KConfig::SimpleConfig); KConfigGroup general2(&config2, "General"); QCOMPARE(bytes, general2.readEntry("Utf8", QByteArray())); } void KConfigTest::testQStringUtf8_data() { QTest::addColumn("data"); QTest::newRow("1") << QByteArray("Téléchargements\tTéléchargements"); QTest::newRow("2") << QByteArray("$¢ह€𐍈\t$¢ह€𐍈"); QTest::newRow("3") << QByteArray("\xc2\xe0\xa4\xf0\x90\x8d\t\\xc2\\xe0\\xa4\\xf0\\x90\\x8d"); // 2 byte overlong QTest::newRow("4") << QByteArray("\xc1\xbf\t\\xc1\\xbf"); // 3 byte overlong QTest::newRow("5") << QByteArray("\xe0\x9f\xbf\t\\xe0\\x9f\\xbf"); // 4 byte overlong QTest::newRow("6") << QByteArray("\xf0\x8f\xbf\xbf\t\\xf0\\x8f\\xbf\\xbf"); // outside unicode range QTest::newRow("7") << QByteArray("\xf4\x90\x80\x80\t\\xf4\\x90\\x80\\x80"); // just within range QTest::newRow("8") << QByteArray("\xc2\x80\t\xc2\x80"); QTest::newRow("9") << QByteArray("\xe0\xa0\x80\t\xe0\xa0\x80"); QTest::newRow("10") << QByteArray("\xf0\x90\x80\x80\t\xf0\x90\x80\x80"); QTest::newRow("11") << QByteArray("\xf4\x8f\xbf\xbf\t\xf4\x8f\xbf\xbf"); } void KConfigTest::testQStringUtf8() { QFETCH(QByteArray, data); const QList d = data.split('\t'); const QByteArray value = d[0]; const QByteArray serialized = d[1]; QTemporaryFile file; QVERIFY(file.open()); KConfig config(file.fileName(), KConfig::SimpleConfig); KConfigGroup general(&config, "General"); general.writeEntry("key", value); config.sync(); file.flush(); file.close(); QFile readFile(file.fileName()); QVERIFY(readFile.open(QFile::ReadOnly)); QByteArray fileBytes = readFile.readAll(); #ifdef Q_OS_WIN fileBytes.replace("\r\n", "\n"); #endif QCOMPARE(fileBytes, QByteArrayLiteral("[General]\nkey=") + serialized + QByteArrayLiteral("\n")); // check that reading works KConfig config2(file.fileName(), KConfig::SimpleConfig); KConfigGroup general2(&config2, "General"); QCOMPARE(value, general2.readEntry("key", QByteArray())); } void KConfigTest::testNewlines() { // test that kconfig always uses the native line endings QTemporaryFile file; QVERIFY(file.open()); KConfig anonConfig(file.fileName(), KConfig::SimpleConfig); KConfigGroup general(&anonConfig, "General"); general.writeEntry("Foo", "Bar"); general.writeEntry("Bar", "Foo"); anonConfig.sync(); file.flush(); file.close(); QFile readFile(file.fileName()); QVERIFY(readFile.open(QFile::ReadOnly)); #ifndef Q_OS_WIN QCOMPARE(readFile.readAll(), QByteArrayLiteral("[General]\nBar=Foo\nFoo=Bar\n")); #else QCOMPARE(readFile.readAll(), QByteArrayLiteral("[General]\r\nBar=Foo\r\nFoo=Bar\r\n")); #endif } void KConfigTest::testXdgListEntry() { QTemporaryFile file; QVERIFY(file.open()); QTextStream out(&file); out << "[General]\n" << "Key1=\n" // empty list // emtpty entries << "Key2=;\n" << "Key3=;;\n" << "Key4=;;;\n" << "Key5=\\;\n" << "Key6=1;2\\;3;;\n"; out.flush(); file.close(); KConfig anonConfig(file.fileName(), KConfig::SimpleConfig); KConfigGroup grp = anonConfig.group("General"); QStringList invalidList; // use this as a default when an empty list is expected invalidList << QStringLiteral("Error! Default value read!"); QCOMPARE(grp.readXdgListEntry("Key1", invalidList), QStringList()); QCOMPARE(grp.readXdgListEntry("Key2", invalidList), QStringList() << QString()); QCOMPARE(grp.readXdgListEntry("Key3", invalidList), QStringList() << QString() << QString()); QCOMPARE(grp.readXdgListEntry("Key4", invalidList), QStringList()<< QString() << QString() << QString()); QCOMPARE(grp.readXdgListEntry("Key5", invalidList), QStringList() << ";"); QCOMPARE(grp.readXdgListEntry("Key6", invalidList), QStringList() << "1" << "2;3" << QString()); } #include #include // To find multithreading bugs: valgrind --tool=helgrind --track-lockorders=no ./kconfigtest testThreads void KConfigTest::testThreads() { QThreadPool::globalInstance()->setMaxThreadCount(6); QList > futures; // Run in parallel some tests that work on different config files, // otherwise unexpected things might indeed happen. futures << QtConcurrent::run(this, &KConfigTest::testAddConfigSources); futures << QtConcurrent::run(this, &KConfigTest::testSimple); futures << QtConcurrent::run(this, &KConfigTest::testDefaults); futures << QtConcurrent::run(this, &KConfigTest::testSharedConfig); futures << QtConcurrent::run(this, &KConfigTest::testSharedConfig); // QEXPECT_FAIL triggers race conditions, it should be fixed to use QThreadStorage... //futures << QtConcurrent::run(this, &KConfigTest::testDeleteWhenLocalized); //futures << QtConcurrent::run(this, &KConfigTest::testEntryMap); for (QFuture f : qAsConst(futures)) { // krazy:exclude=foreach f.waitForFinished(); } } void KConfigTest::testNotify() { #if !KCONFIG_USE_DBUS QSKIP("KConfig notification requires DBus"); #endif KConfig config(TEST_SUBDIR "kconfigtest"); auto myConfigGroup = KConfigGroup(&config, "TopLevelGroup"); //mimics a config in another process, which is watching for events auto remoteConfig = KSharedConfig::openConfig(TEST_SUBDIR "kconfigtest"); KConfigWatcher::Ptr watcher = KConfigWatcher::create(remoteConfig); //some random config that shouldn't be changing when kconfigtest changes, only on kdeglobals auto otherRemoteConfig = KSharedConfig::openConfig(TEST_SUBDIR "kconfigtest2"); KConfigWatcher::Ptr otherWatcher = KConfigWatcher::create(otherRemoteConfig); QSignalSpy watcherSpy(watcher.data(), &KConfigWatcher::configChanged); QSignalSpy otherWatcherSpy(otherWatcher.data(), &KConfigWatcher::configChanged); //write entries in a group and subgroup myConfigGroup.writeEntry("entryA", "foo", KConfig::Persistent | KConfig::Notify); auto subGroup = myConfigGroup.group("aSubGroup"); subGroup.writeEntry("entry1", "foo", KConfig::Persistent | KConfig::Notify); subGroup.writeEntry("entry2", "foo", KConfig::Persistent | KConfig::Notify); config.sync(); watcherSpy.wait(); QCOMPARE(watcherSpy.count(), 2); std::sort(watcherSpy.begin(), watcherSpy.end(), [] (QList a, QList b) { return a[0].value().name() < b[0].value().name(); }); QCOMPARE(watcherSpy[0][0].value().name(), QStringLiteral("TopLevelGroup")); QCOMPARE(watcherSpy[0][1].value(), QByteArrayList({"entryA"})); QCOMPARE(watcherSpy[1][0].value().name(), QStringLiteral("aSubGroup")); QCOMPARE(watcherSpy[1][0].value().parent().name(), QStringLiteral("TopLevelGroup")); QCOMPARE(watcherSpy[1][1].value(), QByteArrayList({"entry1", "entry2"})); //delete an entry watcherSpy.clear(); myConfigGroup.deleteEntry("entryA", KConfig::Persistent | KConfig::Notify); config.sync(); watcherSpy.wait(); QCOMPARE(watcherSpy.count(), 1); QCOMPARE(watcherSpy[0][0].value().name(), QStringLiteral("TopLevelGroup")); QCOMPARE(watcherSpy[0][1].value(), QByteArrayList({"entryA"})); //revert to default an entry watcherSpy.clear(); myConfigGroup.revertToDefault("entryA", KConfig::Persistent | KConfig::Notify); config.sync(); watcherSpy.wait(); QCOMPARE(watcherSpy.count(), 1); QCOMPARE(watcherSpy[0][0].value().name(), QStringLiteral("TopLevelGroup")); QCOMPARE(watcherSpy[0][1].value(), QByteArrayList({"entryA"})); //deleting a group, should notify that every entry in that group has changed watcherSpy.clear(); myConfigGroup.deleteGroup("aSubGroup", KConfig::Persistent | KConfig::Notify); config.sync(); watcherSpy.wait(); QCOMPARE(watcherSpy.count(), 1); QCOMPARE(watcherSpy[0][0].value().name(), QStringLiteral("aSubGroup")); QCOMPARE(watcherSpy[0][1].value(), QByteArrayList({"entry1", "entry2"})); //global write still triggers our notification watcherSpy.clear(); myConfigGroup.writeEntry("someGlobalEntry", "foo", KConfig::Persistent | KConfig::Notify | KConfig::Global); config.sync(); watcherSpy.wait(); QCOMPARE(watcherSpy.count(), 1); QCOMPARE(watcherSpy[0][0].value().name(), QStringLiteral("TopLevelGroup")); QCOMPARE(watcherSpy[0][1].value(), QByteArrayList({"someGlobalEntry"})); //watching another file should have only triggered from the kdeglobals change QCOMPARE(otherWatcherSpy.count(), 1); QCOMPARE(otherWatcherSpy[0][0].value().name(), QStringLiteral("TopLevelGroup")); QCOMPARE(otherWatcherSpy[0][1].value(), QByteArrayList({"someGlobalEntry"})); } + +void KConfigTest::testKdeglobalsVsDefault() +{ + // Add testRestore key with global value in kdeglobals + KConfig glob(QStringLiteral("kdeglobals")); + KConfigGroup generalGlob(&glob, "General"); + generalGlob.writeEntry("testRestore", "global"); + QVERIFY(glob.sync()); + + KConfig local(QStringLiteral(TEST_SUBDIR "restorerc")); + KConfigGroup generalLocal(&local, "General"); + // Check if we get global and not the default value from cpp (defaultcpp) when reading data from restorerc + QCOMPARE(generalLocal.readEntry("testRestore", "defaultcpp"), "global"); + + // Add test restore key with restore value in restorerc file + generalLocal.writeEntry("testRestore", "restore"); + QVERIFY(local.sync()); + local.reparseConfiguration(); + // We expect to get the value from restorerc file + QCOMPARE(generalLocal.readEntry("testRestore", "defaultcpp"), "restore"); + + // Revert to default testRestore key and we expect to get default value and not the global one + generalLocal.revertToDefault("testRestore"); + local.sync(); + local.reparseConfiguration(); + QCOMPARE(generalLocal.readEntry("testRestore", "defaultcpp"), "defaultcpp"); +} diff --git a/autotests/kconfigtest.h b/autotests/kconfigtest.h index e97e4a5..7617d0e 100644 --- a/autotests/kconfigtest.h +++ b/autotests/kconfigtest.h @@ -1,81 +1,83 @@ /* This file is part of the KDE libraries SPDX-FileCopyrightText: 1997 Matthias Kalle Dalheimer SPDX-License-Identifier: LGPL-2.0-or-later */ #ifndef KCONFIGTEST_H #define KCONFIGTEST_H #include class KConfigTest : public QObject { Q_OBJECT public: enum Testing { Ones = 1, Tens = 10, Hundreds = 100}; Q_ENUM(Testing) enum bits { bit0 = 1, bit1 = 2, bit2 = 4, bit3 = 8 }; Q_DECLARE_FLAGS(Flags, bits) Q_FLAG(Flags) private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void testSimple(); void testDefaults(); void testLists(); void testLocale(); void testEncoding(); void testPath(); void testPathQtHome(); void testPersistenceOfExpandFlagForPath(); void testComplex(); void testEnums(); void testEntryMap(); void testInvalid(); void testDeleteEntry(); void testDelete(); void testDeleteWhenLocalized(); void testDefaultGroup(); void testEmptyGroup(); void testCascadingWithLocale(); void testMerge(); void testImmutable(); void testGroupEscape(); void testRevertAllEntries(); void testChangeGroup(); void testGroupCopyTo(); void testConfigCopyTo(); void testConfigCopyToSync(); void testReparent(); void testAnonymousConfig(); void testQByteArrayUtf8(); void testQStringUtf8_data(); void testQStringUtf8(); void testSubGroup(); void testAddConfigSources(); void testWriteOnSync(); void testFailOnReadOnlyFileSync(); void testDirtyOnEqual(); void testDirtyOnEqualOverdo(); void testCreateDir(); void testSharedConfig(); void testOptionOrder(); void testLocaleConfig(); void testDirtyAfterRevert(); void testKdeGlobals(); void testNewlines(); void testXdgListEntry(); void testNotify(); void testThreads(); + void testKdeglobalsVsDefault(); + // should be last void testSyncOnExit(); }; Q_DECLARE_OPERATORS_FOR_FLAGS(KConfigTest::Flags) #endif /* KCONFIGTEST_H */ diff --git a/src/core/kconfigdata.cpp b/src/core/kconfigdata.cpp index bfa662a..5ead816 100644 --- a/src/core/kconfigdata.cpp +++ b/src/core/kconfigdata.cpp @@ -1,328 +1,332 @@ /* This file is part of the KDE libraries SPDX-FileCopyrightText: 2006, 2007 Thomas Braxton SPDX-FileCopyrightText: 1999-2000 Preston Brown SPDX-FileCopyrightText: 1996-2000 Matthias Kalle Dalheimer SPDX-License-Identifier: LGPL-2.0-or-later */ #include QDebug operator<<(QDebug dbg, const KEntryKey &key) { dbg.nospace() << "[" << key.mGroup << ", " << key.mKey << (key.bLocal ? " localized" : "") << (key.bDefault ? " default" : "") << (key.bRaw ? " raw" : "") << "]"; return dbg.space(); } QDebug operator<<(QDebug dbg, const KEntry &entry) { dbg.nospace() << "[" << entry.mValue << (entry.bDirty ? " dirty" : "") << (entry.bGlobal ? " global" : "") << (entry.bImmutable ? " immutable" : "") << (entry.bDeleted ? " deleted" : "") << (entry.bReverted ? " reverted" : "") << (entry.bExpand ? " expand" : "") << "]"; return dbg.space(); } QMap< KEntryKey, KEntry >::Iterator KEntryMap::findExactEntry(const QByteArray &group, const QByteArray &key, KEntryMap::SearchFlags flags) { KEntryKey theKey(group, key, bool(flags & SearchLocalized), bool(flags & SearchDefaults)); return find(theKey); } QMap< KEntryKey, KEntry >::Iterator KEntryMap::findEntry(const QByteArray &group, const QByteArray &key, KEntryMap::SearchFlags flags) { KEntryKey theKey(group, key, false, bool(flags & SearchDefaults)); // try the localized key first if (flags & SearchLocalized) { theKey.bLocal = true; Iterator it = find(theKey); if (it != end()) { return it; } theKey.bLocal = false; } return find(theKey); } QMap< KEntryKey, KEntry >::ConstIterator KEntryMap::findEntry(const QByteArray &group, const QByteArray &key, KEntryMap::SearchFlags flags) const { KEntryKey theKey(group, key, false, bool(flags & SearchDefaults)); // try the localized key first if (flags & SearchLocalized) { theKey.bLocal = true; ConstIterator it = find(theKey); if (it != constEnd()) { return it; } theKey.bLocal = false; } return find(theKey); } bool KEntryMap::setEntry(const QByteArray &group, const QByteArray &key, const QByteArray &value, KEntryMap::EntryOptions options) { KEntryKey k; KEntry e; bool newKey = false; const Iterator it = findExactEntry(group, key, SearchFlags(options >> 16)); if (key.isEmpty()) { // inserting a group marker k.mGroup = group; e.bImmutable = (options & EntryImmutable); if (options & EntryDeleted) { qWarning("Internal KConfig error: cannot mark groups as deleted"); } if (it == end()) { insert(k, e); return true; } else if (it.value() == e) { return false; } it.value() = e; return true; } if (it != end()) { if (it->bImmutable) { return false; // we cannot change this entry. Inherits group immutability. } k = it.key(); e = *it; //qDebug() << "found existing entry for key" << k; + // If overridden entry is global and not default. And it's overridden by a non global + if (e.bGlobal && !(options & EntryGlobal) && !k.bDefault) { + e.bOverridesGlobal = true; + } } else { // make sure the group marker is in the map KEntryMap const *that = this; ConstIterator cit = that->findEntry(group); if (cit == constEnd()) { insert(KEntryKey(group), KEntry()); } else if (cit->bImmutable) { return false; // this group is immutable, so we cannot change this entry. } k = KEntryKey(group, key); newKey = true; } // set these here, since we may be changing the type of key from the one we found k.bLocal = (options & EntryLocalized); k.bDefault = (options & EntryDefault); k.bRaw = (options & EntryRawKey); e.mValue = value; e.bDirty = e.bDirty || (options & EntryDirty); e.bNotify = e.bNotify || (options & EntryNotify); e.bGlobal = (options & EntryGlobal); //we can't use || here, because changes to entries in //kdeglobals would be written to kdeglobals instead //of the local config file, regardless of the globals flag e.bImmutable = e.bImmutable || (options & EntryImmutable); if (value.isNull()) { e.bDeleted = e.bDeleted || (options & EntryDeleted); } else { e.bDeleted = false; // setting a value to a previously deleted entry } e.bExpand = (options & EntryExpansion); e.bReverted = false; if (options & EntryLocalized) { e.bLocalizedCountry = (options & EntryLocalizedCountry); } else { e.bLocalizedCountry = false; } if (newKey) { //qDebug() << "inserting" << k << "=" << value; insert(k, e); if (k.bDefault) { k.bDefault = false; //qDebug() << "also inserting" << k << "=" << value; insert(k, e); } // TODO check for presence of unlocalized key return true; } else { // KEntry e2 = it.value(); if (options & EntryLocalized) { // fast exit checks for cases where the existing entry is more specific const KEntry &e2 = it.value(); if (e2.bLocalizedCountry && !e.bLocalizedCountry) { // lang_COUNTRY > lang return false; } } if (it.value() != e) { //qDebug() << "changing" << k << "from" << e.mValue << "to" << value; it.value() = e; if (k.bDefault) { KEntryKey nonDefaultKey(k); nonDefaultKey.bDefault = false; insert(nonDefaultKey, e); } if (!(options & EntryLocalized)) { KEntryKey theKey(group, key, true, false); //qDebug() << "non-localized entry, remove localized one:" << theKey; remove(theKey); if (k.bDefault) { theKey.bDefault = true; remove(theKey); } } return true; } else { //qDebug() << k << "was already set to" << e.mValue; if (!(options & EntryLocalized)) { //qDebug() << "unchanged non-localized entry, remove localized one."; KEntryKey theKey(group, key, true, false); bool ret = false; Iterator cit = find(theKey); if (cit != end()) { erase(cit); ret = true; } if (k.bDefault) { theKey.bDefault = true; Iterator cit = find(theKey); if (cit != end()) { erase(cit); return true; } } return ret; } //qDebug() << "localized entry, unchanged, return false"; // When we are writing a default, we know that the non- // default is the same as the default, so we can simply // use the same branch. return false; } } } QString KEntryMap::getEntry(const QByteArray &group, const QByteArray &key, const QString &defaultValue, KEntryMap::SearchFlags flags, bool *expand) const { const ConstIterator it = findEntry(group, key, flags); QString theValue = defaultValue; if (it != constEnd() && !it->bDeleted) { if (!it->mValue.isNull()) { const QByteArray data = it->mValue; theValue = QString::fromUtf8(data.constData(), data.length()); if (expand) { *expand = it->bExpand; } } } return theValue; } bool KEntryMap::hasEntry(const QByteArray &group, const QByteArray &key, KEntryMap::SearchFlags flags) const { const ConstIterator it = findEntry(group, key, flags); if (it == constEnd()) { return false; } if (it->bDeleted) { return false; } if (key.isNull()) { // looking for group marker return it->mValue.isNull(); } // if it->bReverted, we'll just return true; the real answer depends on lookup up with SearchDefaults, though. return true; } bool KEntryMap::getEntryOption(const QMap< KEntryKey, KEntry >::ConstIterator &it, KEntryMap::EntryOption option) const { if (it != constEnd()) { switch (option) { case EntryDirty: return it->bDirty; case EntryLocalized: return it.key().bLocal; case EntryGlobal: return it->bGlobal; case EntryImmutable: return it->bImmutable; case EntryDeleted: return it->bDeleted; case EntryExpansion: return it->bExpand; case EntryNotify: return it->bNotify; default: break; // fall through } } return false; } void KEntryMap::setEntryOption(QMap< KEntryKey, KEntry >::Iterator it, KEntryMap::EntryOption option, bool bf) { if (it != end()) { switch (option) { case EntryDirty: it->bDirty = bf; break; case EntryGlobal: it->bGlobal = bf; break; case EntryImmutable: it->bImmutable = bf; break; case EntryDeleted: it->bDeleted = bf; break; case EntryExpansion: it->bExpand = bf; break; case EntryNotify: it->bNotify = bf; break; default: break; // fall through } } } bool KEntryMap::revertEntry(const QByteArray &group, const QByteArray &key, KEntryMap::EntryOptions options, KEntryMap::SearchFlags flags) { Q_ASSERT((flags & KEntryMap::SearchDefaults) == 0); Iterator entry = findEntry(group, key, flags); if (entry != end()) { //qDebug() << "reverting" << entry.key() << " = " << entry->mValue; if (entry->bReverted) { // already done before return false; } KEntryKey defaultKey(entry.key()); defaultKey.bDefault = true; //qDebug() << "looking up default entry with key=" << defaultKey; const ConstIterator defaultEntry = constFind(defaultKey); if (defaultEntry != constEnd()) { Q_ASSERT(defaultEntry.key().bDefault); //qDebug() << "found, update entry"; *entry = *defaultEntry; // copy default value, for subsequent lookups } else { entry->mValue = QByteArray(); } entry->bNotify = entry->bNotify || (options & EntryNotify); entry->bDirty = true; entry->bReverted = true; // skip it when writing out to disk //qDebug() << "Here's what we have now:" << *this; return true; } return false; } diff --git a/src/core/kconfigdata.h b/src/core/kconfigdata.h index d92076d..2f36b1b 100644 --- a/src/core/kconfigdata.h +++ b/src/core/kconfigdata.h @@ -1,234 +1,239 @@ /* This file is part of the KDE libraries SPDX-FileCopyrightText: 2006, 2007 Thomas Braxton SPDX-FileCopyrightText: 1999-2000 Preston Brown SPDX-FileCopyrightText: 1996-2000 Matthias Kalle Dalheimer SPDX-License-Identifier: LGPL-2.0-or-later */ #ifndef KCONFIGDATA_H #define KCONFIGDATA_H #include #include #include #include /** * map/dict/list config node entry. * @internal */ struct KEntry { /** Constructor. @internal */ KEntry() : mValue(), bDirty(false), bGlobal(false), bImmutable(false), bDeleted(false), bExpand(false), bReverted(false), - bLocalizedCountry(false), bNotify(false) {} + bLocalizedCountry(false), bNotify(false), bOverridesGlobal(false) {} /** @internal */ QByteArray mValue; /** * Must the entry be written back to disk? */ bool bDirty : 1; /** * Entry should be written to the global config file */ bool bGlobal: 1; /** * Entry can not be modified. */ bool bImmutable: 1; /** * Entry has been deleted. */ bool bDeleted: 1; /** * Whether to apply dollar expansion or not. */ bool bExpand: 1; /** * Entry has been reverted to its default value (from a more global file). */ bool bReverted: 1; /** * Entry is for a localized key. If @c false the value references just language e.g. "de", * if @c true the value references language and country, e.g. "de_DE". **/ bool bLocalizedCountry: 1; bool bNotify: 1; + + /** + * Entry will need to be written on a non global file even if it matches default value + */ + bool bOverridesGlobal: 1; }; // These operators are used to check whether an entry which is about // to be written equals the previous value. As such, this intentionally // omits the dirty/notify flag from the comparison. inline bool operator ==(const KEntry &k1, const KEntry &k2) { return k1.bGlobal == k2.bGlobal && k1.bImmutable == k2.bImmutable && k1.bDeleted == k2.bDeleted && k1.bExpand == k2.bExpand && k1.mValue == k2.mValue; } inline bool operator !=(const KEntry &k1, const KEntry &k2) { return !(k1 == k2); } /** * key structure holding both the actual key and the group * to which it belongs. * @internal */ struct KEntryKey { /** Constructor. @internal */ KEntryKey(const QByteArray &_group = QByteArray(), const QByteArray &_key = QByteArray(), bool isLocalized = false, bool isDefault = false) : mGroup(_group), mKey(_key), bLocal(isLocalized), bDefault(isDefault), bRaw(false) { ; } /** * The "group" to which this EntryKey belongs */ QByteArray mGroup; /** * The _actual_ key of the entry in question */ QByteArray mKey; /** * Entry is localised or not */ bool bLocal : 1; /** * Entry indicates if this is a default value. */ bool bDefault: 1; /** @internal * Key is a raw unprocessed key. * @warning this should only be set during merging, never for normal use. */ bool bRaw: 1; }; /** * Compares two KEntryKeys (needed for QMap). The order is localized, localized-default, * non-localized, non-localized-default * @internal */ inline bool operator <(const KEntryKey &k1, const KEntryKey &k2) { int result = qstrcmp(k1.mGroup, k2.mGroup); if (result != 0) { return result < 0; } result = qstrcmp(k1.mKey, k2.mKey); if (result != 0) { return result < 0; } if (k1.bLocal != k2.bLocal) { return k1.bLocal; } return (!k1.bDefault && k2.bDefault); } QDebug operator<<(QDebug dbg, const KEntryKey &key); QDebug operator<<(QDebug dbg, const KEntry &entry); /** * \relates KEntry * type specifying a map of entries (key,value pairs). * The keys are actually a key in a particular config file group together * with the group name. * @internal */ class KEntryMap : public QMap { public: enum SearchFlag { SearchDefaults = 1, SearchLocalized = 2 }; Q_DECLARE_FLAGS(SearchFlags, SearchFlag) enum EntryOption { EntryDirty = 1, EntryGlobal = 2, EntryImmutable = 4, EntryDeleted = 8, EntryExpansion = 16, EntryRawKey = 32, EntryLocalizedCountry = 64, EntryNotify = 128, EntryDefault = (SearchDefaults << 16), EntryLocalized = (SearchLocalized << 16) }; Q_DECLARE_FLAGS(EntryOptions, EntryOption) Iterator findExactEntry(const QByteArray &group, const QByteArray &key = QByteArray(), SearchFlags flags = SearchFlags()); Iterator findEntry(const QByteArray &group, const QByteArray &key = QByteArray(), SearchFlags flags = SearchFlags()); ConstIterator findEntry(const QByteArray &group, const QByteArray &key = QByteArray(), SearchFlags flags = SearchFlags()) const; /** * Returns true if the entry gets dirtied or false in other case */ bool setEntry(const QByteArray &group, const QByteArray &key, const QByteArray &value, EntryOptions options); void setEntry(const QByteArray &group, const QByteArray &key, const QString &value, EntryOptions options) { setEntry(group, key, value.toUtf8(), options); } QString getEntry(const QByteArray &group, const QByteArray &key, const QString &defaultValue = QString(), SearchFlags flags = SearchFlags(), bool *expand = nullptr) const; bool hasEntry(const QByteArray &group, const QByteArray &key = QByteArray(), SearchFlags flags = SearchFlags()) const; bool getEntryOption(const ConstIterator &it, EntryOption option) const; bool getEntryOption(const QByteArray &group, const QByteArray &key, SearchFlags flags, EntryOption option) const { return getEntryOption(findEntry(group, key, flags), option); } void setEntryOption(Iterator it, EntryOption option, bool bf); void setEntryOption(const QByteArray &group, const QByteArray &key, SearchFlags flags, EntryOption option, bool bf) { setEntryOption(findEntry(group, key, flags), option, bf); } bool revertEntry(const QByteArray &group, const QByteArray &key, EntryOptions options, SearchFlags flags = SearchFlags()); }; Q_DECLARE_OPERATORS_FOR_FLAGS(KEntryMap::SearchFlags) Q_DECLARE_OPERATORS_FOR_FLAGS(KEntryMap::EntryOptions) /** * \relates KEntry * type for iterating over keys in a KEntryMap in sorted order. * @internal */ typedef QMap::Iterator KEntryMapIterator; /** * \relates KEntry * type for iterating over keys in a KEntryMap in sorted order. * It is const, thus you cannot change the entries in the iterator, * only examine them. * @internal */ typedef QMap::ConstIterator KEntryMapConstIterator; #endif diff --git a/src/core/kconfigini.cpp b/src/core/kconfigini.cpp index 2cea733..9601d03 100644 --- a/src/core/kconfigini.cpp +++ b/src/core/kconfigini.cpp @@ -1,920 +1,923 @@ /* This file is part of the KDE libraries SPDX-FileCopyrightText: 2006, 2007 Thomas Braxton SPDX-FileCopyrightText: 1999 Preston Brown SPDX-FileCopyrightText: 1997-1999 Matthias Kalle Dalheimer SPDX-License-Identifier: LGPL-2.0-or-later */ #include "kconfigini_p.h" #include "kconfig.h" #include "kconfigbackend_p.h" #include "bufferfragment_p.h" #include "kconfigdata.h" #include "kconfig_core_log_settings.h" #include #include #include #include #include #include #include #include #ifndef Q_OS_WIN #include // getuid, close #endif #include // uid_t #include // open KCONFIGCORE_EXPORT bool kde_kiosk_exception = false; // flag to disable kiosk restrictions static QByteArray lookup(const KConfigIniBackend::BufferFragment fragment, QHash *cache) { auto it = cache->constFind(fragment); if (it != cache->constEnd()) { return it.value(); } return cache->insert(fragment, fragment.toByteArray()).value(); } QString KConfigIniBackend::warningProlog(const QFile &file, int line) { return QStringLiteral("KConfigIni: In file %2, line %1: ") .arg(line).arg(file.fileName()); } KConfigIniBackend::KConfigIniBackend() : KConfigBackend(), lockFile(nullptr) { } KConfigIniBackend::~KConfigIniBackend() { } KConfigBackend::ParseInfo KConfigIniBackend::parseConfig(const QByteArray ¤tLocale, KEntryMap &entryMap, ParseOptions options) { return parseConfig(currentLocale, entryMap, options, false); } // merging==true is the merging that happens at the beginning of writeConfig: // merge changes in the on-disk file with the changes in the KConfig object. KConfigBackend::ParseInfo KConfigIniBackend::parseConfig(const QByteArray ¤tLocale, KEntryMap &entryMap, ParseOptions options, bool merging) { if (filePath().isEmpty() || !QFile::exists(filePath())) { return ParseOk; } const QByteArray currentLanguage = currentLocale.split('_').first(); bool bDefault = options & ParseDefaults; bool allowExecutableValues = options & ParseExpansions; QByteArray currentGroup(""); QFile file(filePath()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return ParseOpenError; } QList immutableGroups; bool fileOptionImmutable = false; bool groupOptionImmutable = false; bool groupSkip = false; int lineNo = 0; // on systems using \r\n as end of line, \r will be taken care of by // trim() below QByteArray buffer = file.readAll(); BufferFragment contents(buffer.data(), buffer.size()); unsigned int len = contents.length(); unsigned int startOfLine = 0; // Reduce memory overhead by making use of implicit sharing // This assumes that config files contain only a small amount of // different fragments which are repeated often. // This is often the case, especially sub groups will all have // the same list of keys and similar values as well. QHash cache; cache.reserve(4096); while (startOfLine < len) { BufferFragment line = contents.split('\n', &startOfLine); line.trim(); lineNo++; // skip empty lines and lines beginning with '#' if (line.isEmpty() || line.at(0) == '#') { continue; } if (line.at(0) == '[') { // found a group groupOptionImmutable = fileOptionImmutable; QByteArray newGroup; int start = 1, end; do { end = start; for (;;) { if (end == line.length()) { qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid group header."; // XXX maybe reset the current group here? goto next_line; } if (line.at(end) == ']') { break; } end++; } if (end + 1 == line.length() && start + 2 == end && line.at(start) == '$' && line.at(start + 1) == 'i') { if (newGroup.isEmpty()) { fileOptionImmutable = !kde_kiosk_exception; } else { groupOptionImmutable = !kde_kiosk_exception; } } else { if (!newGroup.isEmpty()) { newGroup += '\x1d'; } BufferFragment namePart = line.mid(start, end - start); printableToString(&namePart, file, lineNo); newGroup += namePart.toByteArray(); } } while ((start = end + 2) <= line.length() && line.at(end + 1) == '['); currentGroup = newGroup; groupSkip = entryMap.getEntryOption(currentGroup, {}, {}, KEntryMap::EntryImmutable); if (groupSkip && !bDefault) { continue; } if (groupOptionImmutable) // Do not make the groups immutable until the entries from // this file have been added. { immutableGroups.append(currentGroup); } } else { if (groupSkip && !bDefault) { continue; // skip entry } BufferFragment aKey; int eqpos = line.indexOf('='); if (eqpos < 0) { aKey = line; line.clear(); } else { BufferFragment temp = line.left(eqpos); temp.trim(); aKey = temp; line.truncateLeft(eqpos + 1); line.trim(); } if (aKey.isEmpty()) { qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid entry (empty key)"; continue; } KEntryMap::EntryOptions entryOptions = {}; if (groupOptionImmutable) { entryOptions |= KEntryMap::EntryImmutable; } BufferFragment locale; int start; while ((start = aKey.lastIndexOf('[')) >= 0) { int end = aKey.indexOf(']', start); if (end < 0) { qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid entry (missing ']')"; goto next_line; } else if (end > start + 1 && aKey.at(start + 1) == '$') { // found option(s) int i = start + 2; while (i < end) { switch (aKey.at(i)) { case 'i': if (!kde_kiosk_exception) { entryOptions |= KEntryMap::EntryImmutable; } break; case 'e': if (allowExecutableValues) { entryOptions |= KEntryMap::EntryExpansion; } break; case 'd': entryOptions |= KEntryMap::EntryDeleted; aKey.truncate(start); printableToString(&aKey, file, lineNo); entryMap.setEntry(currentGroup, aKey.toByteArray(), QByteArray(), entryOptions); goto next_line; default: break; } i++; } } else { // found a locale if (!locale.isNull()) { qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid entry (second locale!?)"; goto next_line; } locale = aKey.mid(start + 1, end - start - 1); } aKey.truncate(start); } if (eqpos < 0) { // Do this here after [$d] was checked qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, lineNo) << "Invalid entry (missing '=')"; continue; } printableToString(&aKey, file, lineNo); if (!locale.isEmpty()) { if (locale != currentLocale && locale != currentLanguage) { // backward compatibility. C == en_US if (locale.at(0) != 'C' || currentLocale != "en_US") { if (merging) { entryOptions |= KEntryMap::EntryRawKey; } else { goto next_line; // skip this entry if we're not merging } } } } if (!(entryOptions & KEntryMap::EntryRawKey)) { printableToString(&aKey, file, lineNo); } if (options & ParseGlobal) { entryOptions |= KEntryMap::EntryGlobal; } if (bDefault) { entryOptions |= KEntryMap::EntryDefault; } if (!locale.isNull()) { entryOptions |= KEntryMap::EntryLocalized; if (locale.indexOf('_') != -1) { entryOptions |= KEntryMap::EntryLocalizedCountry; } } printableToString(&line, file, lineNo); if (entryOptions & KEntryMap::EntryRawKey) { QByteArray rawKey; rawKey.reserve(aKey.length() + locale.length() + 2); rawKey.append(aKey.toVolatileByteArray()); rawKey.append('[').append(locale.toVolatileByteArray()).append(']'); entryMap.setEntry(currentGroup, rawKey, lookup(line, &cache), entryOptions); } else { entryMap.setEntry(currentGroup, lookup(aKey, &cache), lookup(line, &cache), entryOptions); } } next_line: continue; } // now make sure immutable groups are marked immutable for (const QByteArray &group : qAsConst(immutableGroups)) { entryMap.setEntry(group, QByteArray(), QByteArray(), KEntryMap::EntryImmutable); } return fileOptionImmutable ? ParseImmutable : ParseOk; } void KConfigIniBackend::writeEntries(const QByteArray &locale, QIODevice &file, const KEntryMap &map, bool defaultGroup, bool &firstEntry) { QByteArray currentGroup; bool groupIsImmutable = false; const KEntryMapConstIterator end = map.constEnd(); for (KEntryMapConstIterator it = map.constBegin(); it != end; ++it) { const KEntryKey &key = it.key(); // Either process the default group or all others if ((key.mGroup != "") == defaultGroup) { continue; // skip } // the only thing we care about groups is, is it immutable? if (key.mKey.isNull()) { groupIsImmutable = it->bImmutable; continue; // skip } const KEntry ¤tEntry = *it; if (!defaultGroup && currentGroup != key.mGroup) { if (!firstEntry) { file.putChar('\n'); } currentGroup = key.mGroup; for (int start = 0, end;; start = end + 1) { file.putChar('['); end = currentGroup.indexOf('\x1d', start); if (end < 0) { int cgl = currentGroup.length(); if (currentGroup.at(start) == '$' && cgl - start <= 10) { for (int i = start + 1; i < cgl; i++) { char c = currentGroup.at(i); if (c < 'a' || c > 'z') { goto nope; } } file.write("\\x24"); start++; } nope: file.write(stringToPrintable(currentGroup.mid(start), GroupString)); file.putChar(']'); if (groupIsImmutable) { file.write("[$i]", 4); } file.putChar('\n'); break; } else { file.write(stringToPrintable(currentGroup.mid(start, end - start), GroupString)); file.putChar(']'); } } } firstEntry = false; // it is data for a group if (key.bRaw) { // unprocessed key with attached locale from merge file.write(key.mKey); } else { file.write(stringToPrintable(key.mKey, KeyString)); // Key if (key.bLocal && locale != "C") { // 'C' locale == untranslated file.putChar('['); file.write(locale); // locale tag file.putChar(']'); } } if (currentEntry.bDeleted) { if (currentEntry.bImmutable) { file.write("[$di]", 5); // Deleted + immutable } else { file.write("[$d]", 4); // Deleted } } else { if (currentEntry.bImmutable || currentEntry.bExpand) { file.write("[$", 2); if (currentEntry.bImmutable) { file.putChar('i'); } if (currentEntry.bExpand) { file.putChar('e'); } file.putChar(']'); } file.putChar('='); file.write(stringToPrintable(currentEntry.mValue, ValueString)); } file.putChar('\n'); } } void KConfigIniBackend::writeEntries(const QByteArray &locale, QIODevice &file, const KEntryMap &map) { bool firstEntry = true; // write default group writeEntries(locale, file, map, true, firstEntry); // write all other groups writeEntries(locale, file, map, false, firstEntry); } bool KConfigIniBackend::writeConfig(const QByteArray &locale, KEntryMap &entryMap, WriteOptions options) { Q_ASSERT(!filePath().isEmpty()); KEntryMap writeMap; const bool bGlobal = options & WriteGlobal; // First, reparse the file on disk, to merge our changes with the ones done by other apps // Store the result into writeMap. { ParseOptions opts = ParseExpansions; if (bGlobal) { opts |= ParseGlobal; } ParseInfo info = parseConfig(locale, writeMap, opts, true); if (info != ParseOk) { // either there was an error or the file became immutable return false; } } const KEntryMapIterator end = entryMap.end(); for (KEntryMapIterator it = entryMap.begin(); it != end; ++it) { if (!it.key().mKey.isEmpty() && !it->bDirty) { // not dirty, doesn't overwrite entry in writeMap. skips default entries, too. continue; } const KEntryKey &key = it.key(); // only write entries that have the same "globality" as the file if (it->bGlobal == bGlobal) { - if (it->bReverted) { + if (it->bReverted && it->bOverridesGlobal) { + it->bDeleted = true; + writeMap[key] = *it; + } else if (it->bReverted) { writeMap.remove(key); } else if (!it->bDeleted) { writeMap[key] = *it; } else { KEntryKey defaultKey = key; defaultKey.bDefault = true; if (!entryMap.contains(defaultKey)) { writeMap.remove(key); // remove the deleted entry if there is no default //qDebug() << "Detected as deleted=>removed:" << key.mGroup << key.mKey << "global=" << bGlobal; } else { writeMap[key] = *it; // otherwise write an explicitly deleted entry //qDebug() << "Detected as deleted=>[$d]:" << key.mGroup << key.mKey << "global=" << bGlobal; } } it->bDirty = false; } } // now writeMap should contain only entries to be written // so write it out to disk // check if file exists QFile::Permissions fileMode = QFile::ReadUser | QFile::WriteUser; bool createNew = true; QFileInfo fi(filePath()); if (fi.exists()) { #ifdef Q_OS_WIN //TODO: getuid does not exist on windows, use GetSecurityInfo and GetTokenInformation instead createNew = false; #else if (fi.ownerId() == ::getuid()) { // Preserve file mode if file exists and is owned by user. fileMode = fi.permissions(); } else { // File is not owned by user: // Don't create new file but write to existing file instead. createNew = false; } #endif } if (createNew) { QSaveFile file(filePath()); if (!file.open(QIODevice::WriteOnly)) { return false; } file.setTextModeEnabled(true); // to get eol translation writeEntries(locale, file, writeMap); if (!file.size() && (fileMode == (QFile::ReadUser | QFile::WriteUser))) { // File is empty and doesn't have special permissions: delete it. file.cancelWriting(); if (fi.exists()) { // also remove the old file in case it existed. this can happen // when we delete all the entries in an existing config file. // if we don't do this, then deletions and revertToDefault's // will mysteriously fail QFile::remove(filePath()); } } else { // Normal case: Close the file if (file.commit()) { QFile::setPermissions(filePath(), fileMode); return true; } // Couldn't write. Disk full? qCWarning(KCONFIG_CORE_LOG) << "Couldn't write" << filePath() << ". Disk full?"; return false; } } else { // Open existing file. *DON'T* create it if it suddenly does not exist! #ifdef Q_OS_UNIX int fd = QT_OPEN(QFile::encodeName(filePath()).constData(), O_WRONLY | O_TRUNC); if (fd < 0) { return false; } FILE *fp = ::fdopen(fd, "w"); if (!fp) { QT_CLOSE(fd); return false; } QFile f; if (!f.open(fp, QIODevice::WriteOnly)) { fclose(fp); return false; } writeEntries(locale, f, writeMap); f.close(); fclose(fp); #else QFile f(filePath()); // XXX This is broken - it DOES create the file if it is suddenly gone. if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return false; } f.setTextModeEnabled(true); writeEntries(locale, f, writeMap); #endif } return true; } bool KConfigIniBackend::isWritable() const { const QString filePath = this->filePath(); if (!filePath.isEmpty()) { QFileInfo file(filePath); if (!file.exists()) { // If the file does not exist, check if the deepest // existing dir is writable. QFileInfo dir(file.absolutePath()); while (!dir.exists()) { QString parent = dir.absolutePath(); // Go up. Can't use cdUp() on non-existing dirs. if (parent == dir.filePath()) { // no parent return false; } dir.setFile(parent); } return dir.isDir() && dir.isWritable(); } else { return file.isWritable(); } } return false; } QString KConfigIniBackend::nonWritableErrorMessage() const { return tr("Configuration file \"%1\" not writable.\n").arg(filePath()); } void KConfigIniBackend::createEnclosing() { const QString file = filePath(); if (file.isEmpty()) { return; // nothing to do } // Create the containing dir, maybe it wasn't there QDir dir; dir.mkpath(QFileInfo(file).absolutePath()); } void KConfigIniBackend::setFilePath(const QString &file) { if (file.isEmpty()) { return; } Q_ASSERT(QDir::isAbsolutePath(file)); const QFileInfo info(file); if (info.exists()) { setLocalFilePath(info.canonicalFilePath()); } else { const QString dir = info.dir().canonicalPath(); if (!dir.isEmpty()) setLocalFilePath(dir + QLatin1Char('/') + info.fileName()); else setLocalFilePath(file); } } KConfigBase::AccessMode KConfigIniBackend::accessMode() const { if (filePath().isEmpty()) { return KConfigBase::NoAccess; } if (isWritable()) { return KConfigBase::ReadWrite; } return KConfigBase::ReadOnly; } bool KConfigIniBackend::lock() { Q_ASSERT(!filePath().isEmpty()); if (!lockFile) { lockFile = new QLockFile(filePath() + QLatin1String(".lock")); } lockFile->lock(); return lockFile->isLocked(); } void KConfigIniBackend::unlock() { lockFile->unlock(); delete lockFile; lockFile = nullptr; } bool KConfigIniBackend::isLocked() const { return lockFile && lockFile->isLocked(); } namespace { // serialize an escaped byte at the end of @param data // @param data should have room for 4 bytes char* escapeByte(char* data, unsigned char s) { static const char nibbleLookup[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; *data++ = '\\'; *data++ = 'x'; *data++ = nibbleLookup[s >> 4]; *data++ = nibbleLookup[s & 0x0f]; return data; } // Struct that represents a multi-byte UTF-8 character. // This struct is used to keep track of bytes that seem to be valid // UTF-8. struct Utf8Char { public: unsigned char bytes[4]; unsigned char count; unsigned char charLength; Utf8Char() { clear(); charLength = 0; } void clear() { count = 0; } // Add a byte to the UTF8 character. // When an additional byte leads to an invalid character, return false. bool addByte(unsigned char b) { if (count == 0) { if (b > 0xc1 && (b & 0xe0) == 0xc0) { charLength = 2; } else if ((b & 0xf0) == 0xe0) { charLength = 3; } else if (b < 0xf5 && (b & 0xf8) == 0xf0) { charLength = 4; } else { return false; } bytes[0] = b; count = 1; } else if (count < 4 && (b & 0xc0) == 0x80) { if (count == 1) { if (charLength == 3 && bytes[0] == 0xe0 && b < 0xa0) { return false; // overlong 3 byte sequence } if (charLength == 4) { if (bytes[0] == 0xf0 && b < 0x90) { return false; // overlong 4 byte sequence } if (bytes[0] == 0xf4 && b > 0x8f) { return false; // Unicode value larger than U+10FFFF } } } bytes[count++] = b; } else { return false; } return true; } // Return true if Utf8Char contains one valid character. bool isComplete() { return count > 0 && count == charLength; } // Add the bytes in this UTF8 character in escaped form to data. char* escapeBytes(char* data) { for (unsigned char i = 0; i < count; ++i) { data = escapeByte(data, bytes[i]); } clear(); return data; } // Add the bytes of the UTF8 character to a buffer. // Only call this if isComplete() returns true. char* writeUtf8(char* data) { for (unsigned char i = 0; i < count; ++i) { *data++ = bytes[i]; } clear(); return data; } // Write the bytes in the UTF8 character literally, or, if the // character is not complete, write the escaped bytes. // This is useful to handle the state that remains after handling // all bytes in a buffer. char* write(char* data) { if (isComplete()) { data = writeUtf8(data); } else { data = escapeBytes(data); } return data; } }; } QByteArray KConfigIniBackend::stringToPrintable(const QByteArray &aString, StringType type) { if (aString.isEmpty()) { return aString; } const int l = aString.length(); QByteArray result; // Guesstimated that it's good to avoid data() initialization for a length of l*4 result.resize(l * 4); // Maximum 4x as long as source string due to \x escape sequences const char *s = aString.constData(); int i = 0; char *data = result.data(); char *start = data; // Protect leading space if (s[0] == ' ' && type != GroupString) { *data++ = '\\'; *data++ = 's'; i++; } Utf8Char utf8; for (; i < l; ++i/*, r++*/) { switch (s[i]) { default: if (utf8.addByte(s[i])) { break; } else { data = utf8.escapeBytes(data); } // The \n, \t, \r cases (all < 32) are handled below; we can ignore them here if (((unsigned char)s[i]) < 32) { goto doEscape; } // GroupString and KeyString should be valid UTF-8, but ValueString // can be a bytearray with non-UTF-8 bytes that should be escaped. if (type == ValueString && ((unsigned char)s[i]) >= 127) { goto doEscape; } *data++ = s[i]; break; case '\n': *data++ = '\\'; *data++ = 'n'; break; case '\t': *data++ = '\\'; *data++ = 't'; break; case '\r': *data++ = '\\'; *data++ = 'r'; break; case '\\': *data++ = '\\'; *data++ = '\\'; break; case '=': if (type != KeyString) { *data++ = s[i]; break; } goto doEscape; case '[': case ']': // Above chars are OK to put in *value* strings as plaintext if (type == ValueString) { *data++ = s[i]; break; } doEscape: data = escapeByte(data, s[i]); break; } if (utf8.isComplete()) { data = utf8.writeUtf8(data); } } data = utf8.write(data); *data = 0; result.resize(data - start); // Protect trailing space if (result.endsWith(' ') && type != GroupString) { result.replace(result.length() - 1, 1, "\\s"); } return result; } char KConfigIniBackend::charFromHex(const char *str, const QFile &file, int line) { unsigned char ret = 0; for (int i = 0; i < 2; i++) { ret <<= 4; quint8 c = quint8(str[i]); if (c >= '0' && c <= '9') { ret |= c - '0'; } else if (c >= 'a' && c <= 'f') { ret |= c - 'a' + 0x0a; } else if (c >= 'A' && c <= 'F') { ret |= c - 'A' + 0x0a; } else { QByteArray e(str, 2); e.prepend("\\x"); qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, line) << "Invalid hex character " << c << " in \\x-type escape sequence \"" << e.constData() << "\"."; return 'x'; } } return char(ret); } void KConfigIniBackend::printableToString(BufferFragment *aString, const QFile &file, int line) { if (aString->isEmpty() || aString->indexOf('\\') == -1) { return; } aString->trim(); int l = aString->length(); char *r = aString->data(); char *str = r; for (int i = 0; i < l; i++, r++) { if (str[i] != '\\') { *r = str[i]; } else { // Probable escape sequence i++; if (i >= l) { // Line ends after backslash - stop. *r = '\\'; break; } switch (str[i]) { case 's': *r = ' '; break; case 't': *r = '\t'; break; case 'n': *r = '\n'; break; case 'r': *r = '\r'; break; case '\\': *r = '\\'; break; case ';': // not really an escape sequence, but allowed in .desktop files, don't strip '\;' from the string *r = '\\'; r++; *r = ';'; break; case ',': // not really an escape sequence, but allowed in .desktop files, don't strip '\,' from the string *r = '\\'; r++; *r = ','; break; case 'x': if (i + 2 < l) { *r = charFromHex(str + i + 1, file, line); i += 2; } else { *r = 'x'; i = l - 1; } break; default: *r = '\\'; qCWarning(KCONFIG_CORE_LOG) << warningProlog(file, line) << QStringLiteral("Invalid escape sequence \"\\%1\".").arg(str[i]); } } } aString->truncate(r - aString->constData()); }