diff --git a/src/core/kconfig.cpp b/src/core/kconfig.cpp index bc2871c..bdf89b1 100644 --- a/src/core/kconfig.cpp +++ b/src/core/kconfig.cpp @@ -1,1049 +1,1052 @@ /* This file is part of the KDE libraries Copyright (c) 2006, 2007 Thomas Braxton Copyright (c) 1999 Preston Brown Copyright (c) 1997-1999 Matthias Kalle Dalheimer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kconfig.h" #include "kconfig_p.h" #include "config-kconfig.h" #include #include #ifdef _MSC_VER static inline FILE *popen(const char *cmd, const char *mode) { return _popen(cmd, mode); } static inline int pclose(FILE *stream) { return _pclose(stream); } #else #include #endif #include "kconfigbackend_p.h" #include "kconfiggroup.h" #include #include #include #include #include #include #include #include #include #include #include #if KCONFIG_USE_DBUS #include #include #include #endif bool KConfigPrivate::mappingsRegistered = false; Q_GLOBAL_STATIC(QStringList, s_globalFiles) // For caching purposes. static QBasicMutex s_globalFilesMutex; Q_GLOBAL_STATIC_WITH_ARGS(QString, sGlobalFileName, (QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/kdeglobals"))) #ifndef Q_OS_WIN static const Qt::CaseSensitivity sPathCaseSensitivity = Qt::CaseSensitive; #else static const Qt::CaseSensitivity sPathCaseSensitivity = Qt::CaseInsensitive; #endif KConfigPrivate::KConfigPrivate(KConfig::OpenFlags flags, QStandardPaths::StandardLocation resourceType) : openFlags(flags), resourceType(resourceType), mBackend(nullptr), bDynamicBackend(true), bDirty(false), bReadDefaults(false), bFileImmutable(false), bForceGlobal(false), bSuppressGlobal(false), configState(KConfigBase::NoAccess) { static QBasicAtomicInt use_etc_kderc = Q_BASIC_ATOMIC_INITIALIZER(-1); if (use_etc_kderc.load() < 0) { use_etc_kderc.store( !qEnvironmentVariableIsSet("KDE_SKIP_KDERC")); // for unit tests } if (use_etc_kderc.load()) { etc_kderc = #ifdef Q_OS_WIN QFile::decodeName(qgetenv("WINDIR") + "/kde5rc"); #else QStringLiteral("/etc/kde5rc"); #endif if (!QFileInfo(etc_kderc).isReadable()) { use_etc_kderc.store(false); etc_kderc.clear(); } } // if (!mappingsRegistered) { // KEntryMap tmp; // if (!etc_kderc.isEmpty()) { // QExplicitlySharedDataPointer backend = KConfigBackend::create(etc_kderc, QLatin1String("INI")); // backend->parseConfig( "en_US", tmp, KConfigBackend::ParseDefaults); // } // const QString kde5rc(QDir::home().filePath(".kde5rc")); // if (KStandardDirs::checkAccess(kde5rc, R_OK)) { // QExplicitlySharedDataPointer backend = KConfigBackend::create(kde5rc, QLatin1String("INI")); // backend->parseConfig( "en_US", tmp, KConfigBackend::ParseOptions()); // } // KConfigBackend::registerMappings(tmp); // mappingsRegistered = true; // } setLocale(QLocale().name()); } bool KConfigPrivate::lockLocal() { if (mBackend) { return mBackend->lock(); } // anonymous object - pretend we locked it return true; } void KConfigPrivate::copyGroup(const QByteArray &source, const QByteArray &destination, KConfigGroup *otherGroup, KConfigBase::WriteConfigFlags flags) const { KEntryMap &otherMap = otherGroup->config()->d_ptr->entryMap; const int len = source.length(); const bool sameName = (destination == source); // we keep this bool outside the foreach loop so that if // the group is empty, we don't end up marking the other config // as dirty erroneously bool dirtied = false; for (KEntryMap::ConstIterator entryMapIt(entryMap.constBegin()); entryMapIt != entryMap.constEnd(); ++entryMapIt) { const QByteArray &group = entryMapIt.key().mGroup; if (!group.startsWith(source)) { // nothing to do continue; } // don't copy groups that start with the same prefix, but are not sub-groups if (group.length() > len && group[len] != '\x1d') { continue; } KEntryKey newKey = entryMapIt.key(); if (flags & KConfigBase::Localized) { newKey.bLocal = true; } if (!sameName) { newKey.mGroup.replace(0, len, destination); } KEntry entry = entryMap[ entryMapIt.key() ]; dirtied = entry.bDirty = flags & KConfigBase::Persistent; if (flags & KConfigBase::Global) { entry.bGlobal = true; } otherMap[newKey] = entry; } if (dirtied) { otherGroup->config()->d_ptr->bDirty = true; } } QString KConfigPrivate::expandString(const QString &value) { QString aValue = value; // check for environment variables and make necessary translations int nDollarPos = aValue.indexOf(QLatin1Char('$')); while (nDollarPos != -1 && nDollarPos + 1 < aValue.length()) { // there is at least one $ if (aValue[nDollarPos + 1] == QLatin1Char('(')) { int nEndPos = nDollarPos + 1; // the next character is not $ while ((nEndPos <= aValue.length()) && (aValue[nEndPos] != QLatin1Char(')'))) { nEndPos++; } nEndPos++; QString cmd = aValue.mid(nDollarPos + 2, nEndPos - nDollarPos - 3); QString result; // FIXME: wince does not have pipes #ifndef _WIN32_WCE FILE *fs = popen(QFile::encodeName(cmd).data(), "r"); if (fs) { QTextStream ts(fs, QIODevice::ReadOnly); result = ts.readAll().trimmed(); pclose(fs); } #endif aValue.replace(nDollarPos, nEndPos - nDollarPos, result); nDollarPos += result.length(); } else if (aValue[nDollarPos + 1] != QLatin1Char('$')) { int nEndPos = nDollarPos + 1; // the next character is not $ QStringRef aVarName; if (aValue[nEndPos] == QLatin1Char('{')) { while ((nEndPos <= aValue.length()) && (aValue[nEndPos] != QLatin1Char('}'))) { nEndPos++; } nEndPos++; aVarName = aValue.midRef(nDollarPos + 2, nEndPos - nDollarPos - 3); } else { while (nEndPos <= aValue.length() && (aValue[nEndPos].isNumber() || aValue[nEndPos].isLetter() || aValue[nEndPos] == QLatin1Char('_'))) { nEndPos++; } aVarName = aValue.midRef(nDollarPos + 1, nEndPos - nDollarPos - 1); } QString env; if (!aVarName.isEmpty()) { #ifdef Q_OS_WIN if (aVarName == QLatin1String("HOME")) { env = QDir::homePath(); } else #endif { QByteArray pEnv = qgetenv(aVarName.toLatin1().constData()); if (!pEnv.isEmpty()) { env = QString::fromLocal8Bit(pEnv.constData()); } else { if (aVarName == QStringLiteral("QT_DATA_HOME")) { env = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); } else if (aVarName == QStringLiteral("QT_CONFIG_HOME")) { env = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); } else if (aVarName == QStringLiteral("QT_CACHE_HOME")) { env = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation); } } } aValue.replace(nDollarPos, nEndPos - nDollarPos, env); nDollarPos += env.length(); } else { aValue.remove(nDollarPos, nEndPos - nDollarPos); } } else { // remove one of the dollar signs aValue.remove(nDollarPos, 1); nDollarPos++; } nDollarPos = aValue.indexOf(QLatin1Char('$'), nDollarPos); } return aValue; } KConfig::KConfig(const QString &file, OpenFlags mode, QStandardPaths::StandardLocation resourceType) : d_ptr(new KConfigPrivate(mode, resourceType)) { d_ptr->changeFileName(file); // set the local file name // read initial information off disk reparseConfiguration(); } KConfig::KConfig(const QString &file, const QString &backend, QStandardPaths::StandardLocation resourceType) : d_ptr(new KConfigPrivate(SimpleConfig, resourceType)) { d_ptr->mBackend = KConfigBackend::create(file, backend); d_ptr->bDynamicBackend = false; d_ptr->changeFileName(file); // set the local file name // read initial information off disk reparseConfiguration(); } KConfig::KConfig(KConfigPrivate &d) : d_ptr(&d) { } KConfig::~KConfig() { Q_D(KConfig); if (d->bDirty && (d->mBackend && d->mBackend->ref.load() == 1)) { sync(); } delete d; } QStringList KConfig::groupList() const { Q_D(const KConfig); QSet groups; for (KEntryMap::ConstIterator entryMapIt(d->entryMap.constBegin()); entryMapIt != d->entryMap.constEnd(); ++entryMapIt) { const KEntryKey &key = entryMapIt.key(); const QByteArray group = key.mGroup; if (key.mKey.isNull() && !group.isEmpty() && group != "" && group != "$Version") { const QString groupname = QString::fromUtf8(group); groups << groupname.left(groupname.indexOf(QLatin1Char('\x1d'))); } } return groups.toList(); } QStringList KConfigPrivate::groupList(const QByteArray &group) const { QByteArray theGroup = group + '\x1d'; QSet groups; for (KEntryMap::ConstIterator entryMapIt(entryMap.constBegin()); entryMapIt != entryMap.constEnd(); ++entryMapIt) { const KEntryKey &key = entryMapIt.key(); if (key.mKey.isNull() && key.mGroup.startsWith(theGroup)) { const QString groupname = QString::fromUtf8(key.mGroup.mid(theGroup.length())); groups << groupname.left(groupname.indexOf(QLatin1Char('\x1d'))); } } return groups.toList(); } static bool isGroupOrSubGroupMatch(const QByteArray &potentialGroup, const QByteArray &group) { if (!potentialGroup.startsWith(group)) { return false; } return potentialGroup.length() == group.length() || potentialGroup[group.length()] == '\x1d'; } // List all sub groups, including subsubgroups QSet KConfigPrivate::allSubGroups(const QByteArray &parentGroup) const { QSet groups; for (KEntryMap::const_iterator entryMapIt = entryMap.begin(); entryMapIt != entryMap.end(); ++entryMapIt) { const KEntryKey &key = entryMapIt.key(); if (key.mKey.isNull() && isGroupOrSubGroupMatch(key.mGroup, parentGroup)) { groups << key.mGroup; } } return groups; } bool KConfigPrivate::hasNonDeletedEntries(const QByteArray &group) const { for (KEntryMap::const_iterator it = entryMap.begin(); it != entryMap.end(); ++it) { const KEntryKey &key = it.key(); // Check for any non-deleted entry if (isGroupOrSubGroupMatch(key.mGroup, group) && !key.mKey.isNull() && !it->bDeleted) { return true; } } return false; } QStringList KConfigPrivate::keyListImpl(const QByteArray &theGroup) const { QStringList keys; const KEntryMapConstIterator theEnd = entryMap.constEnd(); KEntryMapConstIterator it = entryMap.findEntry(theGroup); if (it != theEnd) { ++it; // advance past the special group entry marker QSet tmp; for (; it != theEnd && it.key().mGroup == theGroup; ++it) { const KEntryKey &key = it.key(); if (!key.mKey.isNull() && !it->bDeleted) { tmp << QString::fromUtf8(key.mKey); } } keys = tmp.toList(); } return keys; } QStringList KConfig::keyList(const QString &aGroup) const { Q_D(const KConfig); const QByteArray theGroup(aGroup.isEmpty() ? "" : aGroup.toUtf8()); return d->keyListImpl(theGroup); } QMap KConfig::entryMap(const QString &aGroup) const { Q_D(const KConfig); QMap theMap; const QByteArray theGroup(aGroup.isEmpty() ? "" : aGroup.toUtf8()); const KEntryMapConstIterator theEnd = d->entryMap.constEnd(); KEntryMapConstIterator it = d->entryMap.findEntry(theGroup, nullptr, nullptr); if (it != theEnd) { ++it; // advance past the special group entry marker for (; it != theEnd && it.key().mGroup == theGroup; ++it) { // leave the default values and deleted entries out if (!it->bDeleted && !it.key().bDefault) { const QString key = QString::fromUtf8(it.key().mKey.constData()); // the localized entry should come first, so don't overwrite it // with the non-localized entry if (!theMap.contains(key)) { if (it->bExpand) { theMap.insert(key, KConfigPrivate::expandString(QString::fromUtf8(it->mValue.constData()))); } else { theMap.insert(key, QString::fromUtf8(it->mValue.constData())); } } } } } return theMap; } bool KConfig::sync() { Q_D(KConfig); if (isImmutable() || name().isEmpty()) { // can't write to an immutable or anonymous file. return false; } QHash notifyGroupsLocal; QHash notifyGroupsGlobal; if (d->bDirty && d->mBackend) { const QByteArray utf8Locale(locale().toUtf8()); // Create the containing dir, maybe it wasn't there d->mBackend->createEnclosing(); // lock the local file if (d->configState == ReadWrite && !d->lockLocal()) { qWarning() << "couldn't lock local file"; return false; } // Rewrite global/local config only if there is a dirty entry in it. bool writeGlobals = false; bool writeLocals = false; for (auto it = d->entryMap.constBegin(); it != d->entryMap.constEnd(); ++it) { auto e = it.value(); if (e.bDirty) { if (e.bGlobal) { writeGlobals = true; if (e.bNotify) { notifyGroupsGlobal[QString::fromUtf8(it.key().mGroup)] << it.key().mKey; } } else { writeLocals = true; if (e.bNotify) { notifyGroupsLocal[QString::fromUtf8(it.key().mGroup)] << it.key().mKey; } } } } d->bDirty = false; // will revert to true if a config write fails if (d->wantGlobals() && writeGlobals) { QExplicitlySharedDataPointer tmp = KConfigBackend::create(*sGlobalFileName); if (d->configState == ReadWrite && !tmp->lock()) { qWarning() << "couldn't lock global file"; //unlock the local config if we're returning early if (d->mBackend->isLocked()) { d->mBackend->unlock(); } d->bDirty = true; return false; } if (!tmp->writeConfig(utf8Locale, d->entryMap, KConfigBackend::WriteGlobal)) { d->bDirty = true; } if (tmp->isLocked()) { tmp->unlock(); } } if (writeLocals) { if (!d->mBackend->writeConfig(utf8Locale, d->entryMap, KConfigBackend::WriteOptions())) { d->bDirty = true; } } if (d->mBackend->isLocked()) { d->mBackend->unlock(); } } if (!notifyGroupsLocal.isEmpty()) { d->notifyClients(notifyGroupsLocal, QStringLiteral("/") + name()); } if (!notifyGroupsGlobal.isEmpty()) { d->notifyClients(notifyGroupsGlobal, QStringLiteral("/kdeglobals")); } return !d->bDirty; } void KConfigPrivate::notifyClients(const QHash &changes, const QString &path) { #if KCONFIG_USE_DBUS qDBusRegisterMetaType(); qDBusRegisterMetaType>(); QDBusMessage message = QDBusMessage::createSignal(path, QStringLiteral("org.kde.kconfig.notify"), QStringLiteral("ConfigChanged")); message.setArguments({QVariant::fromValue(changes)}); QDBusConnection::sessionBus().send(message); #else Q_UNUSED(changes) Q_UNUSED(path) #endif } void KConfig::markAsClean() { Q_D(KConfig); d->bDirty = false; // clear any dirty flags that entries might have set const KEntryMapIterator theEnd = d->entryMap.end(); for (KEntryMapIterator it = d->entryMap.begin(); it != theEnd; ++it) { it->bDirty = false; it->bNotify = false; } } bool KConfig::isDirty() const { Q_D(const KConfig); return d->bDirty; } void KConfig::checkUpdate(const QString &id, const QString &updateFile) { const KConfigGroup cg(this, "$Version"); const QString cfg_id = updateFile + QLatin1Char(':') + id; const QStringList ids = cg.readEntry("update_info", QStringList()); if (!ids.contains(cfg_id)) { QProcess::execute(QStringLiteral(KCONF_UPDATE_INSTALL_LOCATION), QStringList() << QStringLiteral("--check") << updateFile); reparseConfiguration(); } } KConfig *KConfig::copyTo(const QString &file, KConfig *config) const { Q_D(const KConfig); if (!config) { config = new KConfig(QString(), SimpleConfig, d->resourceType); } config->d_func()->changeFileName(file); config->d_func()->entryMap = d->entryMap; config->d_func()->bFileImmutable = false; const KEntryMapIterator theEnd = config->d_func()->entryMap.end(); for (KEntryMapIterator it = config->d_func()->entryMap.begin(); it != theEnd; ++it) { it->bDirty = true; } config->d_ptr->bDirty = true; return config; } QString KConfig::name() const { Q_D(const KConfig); return d->fileName; } KConfig::OpenFlags KConfig::openFlags() const { Q_D(const KConfig); return d->openFlags; } struct KConfigStaticData { QString globalMainConfigName; // Keep a copy so we can use it in global dtors, after qApp is gone QStringList appArgs; }; Q_GLOBAL_STATIC(KConfigStaticData, globalData) void KConfig::setMainConfigName(const QString &str) { globalData()->globalMainConfigName = str; } QString KConfig::mainConfigName() { KConfigStaticData* data = globalData(); if (data->appArgs.isEmpty()) data->appArgs = QCoreApplication::arguments(); // --config on the command line overrides everything else const QStringList args = data->appArgs; for (int i = 1; i < args.count(); ++i) { if (args.at(i) == QLatin1String("--config") && i < args.count() - 1) { return args.at(i + 1); } } const QString globalName = data->globalMainConfigName; if (!globalName.isEmpty()) { return globalName; } QString appName = QCoreApplication::applicationName(); return appName + QLatin1String("rc"); } void KConfigPrivate::changeFileName(const QString &name) { fileName = name; QString file; if (name.isEmpty()) { if (wantDefaults()) { // accessing default app-specific config "appnamerc" fileName = KConfig::mainConfigName(); file = QStandardPaths::writableLocation(resourceType) + QLatin1Char('/') + fileName; } else if (wantGlobals()) { // accessing "kdeglobals" by specifying no filename and NoCascade - XXX used anywhere? resourceType = QStandardPaths::GenericConfigLocation; fileName = QStringLiteral("kdeglobals"); file = *sGlobalFileName; } else { // anonymous config openFlags = KConfig::SimpleConfig; return; } } else if (QDir::isAbsolutePath(fileName)) { fileName = QFileInfo(fileName).canonicalFilePath(); if (fileName.isEmpty()) { // file doesn't exist (yet) fileName = name; } file = fileName; } else { file = QStandardPaths::writableLocation(resourceType) + QLatin1Char('/') + fileName; } Q_ASSERT(!file.isEmpty()); bSuppressGlobal = (file.compare(*sGlobalFileName, sPathCaseSensitivity) == 0); if (bDynamicBackend || !mBackend) { // allow dynamic changing of backend mBackend = KConfigBackend::create(file); } else { mBackend->setFilePath(file); } configState = mBackend->accessMode(); } void KConfig::reparseConfiguration() { Q_D(KConfig); if (d->fileName.isEmpty()) { return; } // Don't lose pending changes if (!d->isReadOnly() && d->bDirty) { sync(); } d->entryMap.clear(); d->bFileImmutable = false; { QMutexLocker locker(&s_globalFilesMutex); s_globalFiles()->clear(); } // Parse all desired files from the least to the most specific. if (d->wantGlobals()) { d->parseGlobalFiles(); } d->parseConfigFiles(); } QStringList KConfigPrivate::getGlobalFiles() const { QMutexLocker locker(&s_globalFilesMutex); if (s_globalFiles()->isEmpty()) { const QStringList paths1 = QStandardPaths::locateAll(QStandardPaths::GenericConfigLocation, QStringLiteral("kdeglobals")); const QStringList paths2 = QStandardPaths::locateAll(QStandardPaths::GenericConfigLocation, QStringLiteral("system.kdeglobals")); const bool useEtcKderc = !etc_kderc.isEmpty(); s_globalFiles()->reserve(paths1.size() + paths2.size() + (useEtcKderc ? 1 : 0)); Q_FOREACH (const QString &dir1, paths1) { s_globalFiles()->push_front(dir1); } Q_FOREACH (const QString &dir2, paths2) { s_globalFiles()->push_front(dir2); } if (useEtcKderc) { s_globalFiles()->push_front(etc_kderc); } } return *s_globalFiles(); } void KConfigPrivate::parseGlobalFiles() { const QStringList globalFiles = getGlobalFiles(); // qDebug() << "parsing global files" << globalFiles; // TODO: can we cache the values in etc_kderc / other global files // on a per-application basis? const QByteArray utf8Locale = locale.toUtf8(); Q_FOREACH (const QString &file, globalFiles) { KConfigBackend::ParseOptions parseOpts = KConfigBackend::ParseGlobal | KConfigBackend::ParseExpansions; if (file.compare(*sGlobalFileName, sPathCaseSensitivity) != 0) parseOpts |= KConfigBackend::ParseDefaults; QExplicitlySharedDataPointer backend = KConfigBackend::create(file); if (backend->parseConfig(utf8Locale, entryMap, parseOpts) == KConfigBackend::ParseImmutable) { break; } } } void KConfigPrivate::parseConfigFiles() { // can only read the file if there is a backend and a file name if (mBackend && !fileName.isEmpty()) { bFileImmutable = false; QList files; if (wantDefaults()) { if (bSuppressGlobal) { files = getGlobalFiles(); } else { if (QDir::isAbsolutePath(fileName)) { - files << fileName; + const QString canonicalFile = QFileInfo(fileName).canonicalFilePath(); + if (!canonicalFile.isEmpty()) { // empty if it doesn't exist + files << canonicalFile; + } } else { Q_FOREACH (const QString &f, QStandardPaths::locateAll(resourceType, fileName)) { - files.prepend(f); + files.prepend(QFileInfo(f).canonicalFilePath()); } // allow fallback to config files bundled in resources const QString resourceFile(QStringLiteral(":/kconfig/") + fileName); if (QFile::exists(resourceFile)) { files.prepend(resourceFile); } } } } else { files << mBackend->filePath(); } if (!isSimple()) { files = extraFiles.toList() + files; } // qDebug() << "parsing local files" << files; const QByteArray utf8Locale = locale.toUtf8(); foreach (const QString &file, files) { if (file.compare(mBackend->filePath(), sPathCaseSensitivity) == 0) { switch (mBackend->parseConfig(utf8Locale, entryMap, KConfigBackend::ParseExpansions)) { case KConfigBackend::ParseOk: break; case KConfigBackend::ParseImmutable: bFileImmutable = true; break; case KConfigBackend::ParseOpenError: configState = KConfigBase::NoAccess; break; } } else { QExplicitlySharedDataPointer backend = KConfigBackend::create(file); bFileImmutable = (backend->parseConfig(utf8Locale, entryMap, KConfigBackend::ParseDefaults | KConfigBackend::ParseExpansions) == KConfigBackend::ParseImmutable); } if (bFileImmutable) { break; } } } } KConfig::AccessMode KConfig::accessMode() const { Q_D(const KConfig); return d->configState; } void KConfig::addConfigSources(const QStringList &files) { Q_D(KConfig); Q_FOREACH (const QString &file, files) { d->extraFiles.push(file); } if (!files.isEmpty()) { reparseConfiguration(); } } QStringList KConfig::additionalConfigSources() const { Q_D(const KConfig); return d->extraFiles.toList(); } QString KConfig::locale() const { Q_D(const KConfig); return d->locale; } bool KConfigPrivate::setLocale(const QString &aLocale) { if (aLocale != locale) { locale = aLocale; return true; } return false; } bool KConfig::setLocale(const QString &locale) { Q_D(KConfig); if (d->setLocale(locale)) { reparseConfiguration(); return true; } return false; } void KConfig::setReadDefaults(bool b) { Q_D(KConfig); d->bReadDefaults = b; } bool KConfig::readDefaults() const { Q_D(const KConfig); return d->bReadDefaults; } bool KConfig::isImmutable() const { Q_D(const KConfig); return d->bFileImmutable; } bool KConfig::isGroupImmutableImpl(const QByteArray &aGroup) const { Q_D(const KConfig); return isImmutable() || d->entryMap.getEntryOption(aGroup, nullptr, nullptr, KEntryMap::EntryImmutable); } #ifndef KDE_NO_DEPRECATED void KConfig::setForceGlobal(bool b) { Q_D(KConfig); d->bForceGlobal = b; } #endif #ifndef KDE_NO_DEPRECATED bool KConfig::forceGlobal() const { Q_D(const KConfig); return d->bForceGlobal; } #endif KConfigGroup KConfig::groupImpl(const QByteArray &group) { return KConfigGroup(this, group.constData()); } const KConfigGroup KConfig::groupImpl(const QByteArray &group) const { return KConfigGroup(this, group.constData()); } KEntryMap::EntryOptions convertToOptions(KConfig::WriteConfigFlags flags) { KEntryMap::EntryOptions options = nullptr; if (flags & KConfig::Persistent) { options |= KEntryMap::EntryDirty; } if (flags & KConfig::Global) { options |= KEntryMap::EntryGlobal; } if (flags & KConfig::Localized) { options |= KEntryMap::EntryLocalized; } if (flags.testFlag(KConfig::Notify)) { options |= KEntryMap::EntryNotify; } return options; } void KConfig::deleteGroupImpl(const QByteArray &aGroup, WriteConfigFlags flags) { Q_D(KConfig); KEntryMap::EntryOptions options = convertToOptions(flags) | KEntryMap::EntryDeleted; const QSet groups = d->allSubGroups(aGroup); Q_FOREACH (const QByteArray &group, groups) { const QStringList keys = d->keyListImpl(group); Q_FOREACH (const QString &_key, keys) { const QByteArray &key = _key.toUtf8(); if (d->canWriteEntry(group, key.constData())) { d->entryMap.setEntry(group, key, QByteArray(), options); d->bDirty = true; } } } } bool KConfig::isConfigWritable(bool warnUser) { Q_D(KConfig); bool allWritable = (d->mBackend ? d->mBackend->isWritable() : false); if (warnUser && !allWritable) { QString errorMsg; if (d->mBackend) { // TODO how can be it be null? Set errorMsg appropriately errorMsg = d->mBackend->nonWritableErrorMessage(); } // Note: We don't ask the user if we should not ask this question again because we can't save the answer. errorMsg += QCoreApplication::translate("KConfig", "Please contact your system administrator."); QString cmdToExec = QStandardPaths::findExecutable(QStringLiteral("kdialog")); if (!cmdToExec.isEmpty()) { QProcess::execute(cmdToExec, QStringList() << QStringLiteral("--title") << QCoreApplication::applicationName() << QStringLiteral("--msgbox") << errorMsg); } } d->configState = allWritable ? ReadWrite : ReadOnly; // update the read/write status return allWritable; } bool KConfig::hasGroupImpl(const QByteArray &aGroup) const { Q_D(const KConfig); // No need to look for the actual group entry anymore, or for subgroups: // a group exists if it contains any non-deleted entry. return d->hasNonDeletedEntries(aGroup); } bool KConfigPrivate::canWriteEntry(const QByteArray &group, const char *key, bool isDefault) const { if (bFileImmutable || entryMap.getEntryOption(group, key, KEntryMap::SearchLocalized, KEntryMap::EntryImmutable)) { return isDefault; } return true; } void KConfigPrivate::putData(const QByteArray &group, const char *key, const QByteArray &value, KConfigBase::WriteConfigFlags flags, bool expand) { KEntryMap::EntryOptions options = convertToOptions(flags); if (bForceGlobal) { options |= KEntryMap::EntryGlobal; } if (expand) { options |= KEntryMap::EntryExpansion; } if (value.isNull()) { // deleting entry options |= KEntryMap::EntryDeleted; } bool dirtied = entryMap.setEntry(group, key, value, options); if (dirtied && (flags & KConfigBase::Persistent)) { bDirty = true; } } void KConfigPrivate::revertEntry(const QByteArray &group, const char *key) { bool dirtied = entryMap.revertEntry(group, key); if (dirtied) { bDirty = true; } } QByteArray KConfigPrivate::lookupData(const QByteArray &group, const char *key, KEntryMap::SearchFlags flags) const { if (bReadDefaults) { flags |= KEntryMap::SearchDefaults; } const KEntryMapConstIterator it = entryMap.findEntry(group, key, flags); if (it == entryMap.constEnd()) { return QByteArray(); } return it->mValue; } QString KConfigPrivate::lookupData(const QByteArray &group, const char *key, KEntryMap::SearchFlags flags, bool *expand) const { if (bReadDefaults) { flags |= KEntryMap::SearchDefaults; } return entryMap.getEntry(group, key, QString(), flags, expand); } QStandardPaths::StandardLocation KConfig::locationType() const { Q_D(const KConfig); return d->resourceType; } void KConfig::virtual_hook(int /*id*/, void * /*data*/) { /* nothing */ } diff --git a/src/core/kconfigini.cpp b/src/core/kconfigini.cpp index b674973..84d77b4 100644 --- a/src/core/kconfigini.cpp +++ b/src/core/kconfigini.cpp @@ -1,824 +1,828 @@ /* This file is part of the KDE libraries Copyright (c) 2006, 2007 Thomas Braxton Copyright (c) 1999 Preston Brown Copyright (C) 1997-1999 Matthias Kalle Dalheimer (kalle@kde.org) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kconfigini_p.h" #include "kconfig.h" #include "kconfigbackend_p.h" #include "bufferfragment_p.h" #include "kconfigdata.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()) { qWarning() << 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, nullptr, nullptr, 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()) { qWarning() << warningProlog(file, lineNo) << "Invalid entry (empty key)"; continue; } KEntryMap::EntryOptions entryOptions = nullptr; if (groupOptionImmutable) { entryOptions |= KEntryMap::EntryImmutable; } BufferFragment locale; int start; while ((start = aKey.lastIndexOf('[')) >= 0) { int end = aKey.indexOf(']', start); if (end < 0) { qWarning() << 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 = aKey.left(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()) { qWarning() << 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 qWarning() << 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 Q_FOREACH (const QByteArray &group, 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) { 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? qWarning() << "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 { - setLocalFilePath(file); + 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(); } QByteArray KConfigIniBackend::stringToPrintable(const QByteArray &aString, StringType type) { static const char nibbleLookup[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; 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++; } for (; i < l; ++i/*, r++*/) { switch (s[i]) { default: // 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++ = '\\'; *data++ = 'x'; *data++ = nibbleLookup[((unsigned char)s[i]) >> 4]; *data++ = nibbleLookup[((unsigned char)s[i]) & 0x0f]; break; } } *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"); qWarning() << 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 = '\\'; qWarning() << warningProlog(file, line) << QStringLiteral("Invalid escape sequence \"\\%1\".").arg(str[i]); } } } aString->truncate(r - aString->constData()); }